From e1832f2d142787e366348b86e930f23cb5b2f7f1 Mon Sep 17 00:00:00 2001 From: rasputin Date: Tue, 2 Jun 2026 19:28:12 +0200 Subject: [PATCH] refactor --- .idea/dictionaries/project.xml | 1 + .../gudariwallet/ui/HistoryScreen.kt | 132 +++++++-------- .../bitcointxoko/gudariwallet/ui/HomeTab.kt | 155 +++++++++--------- 3 files changed, 145 insertions(+), 143 deletions(-) diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index eef31d2..19ba699 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -6,6 +6,7 @@ Tink amboss hkdf + inflight lnbc lnbits lnurlp diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt index 22c89f1..8ac3b86 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt @@ -2,7 +2,6 @@ package com.bitcointxoko.gudariwallet.ui import android.content.ClipData import android.content.Intent -import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -32,7 +31,7 @@ import androidx.core.net.toUri import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.util.feePpm -import com.bitcointxoko.gudariwallet.util.formatFiatForSats // ← new import +import com.bitcointxoko.gudariwallet.util.formatFiatForSats import kotlinx.coroutines.launch // ── List screen ────────────────────────────────────────────────────────────── @@ -156,6 +155,21 @@ fun HistoryScreen( } } +// ── Payment color helpers ───────────────────────────────────────────────────── + +private fun isPendingStatus(status: String): Boolean = + status.lowercase() in setOf("pending", "in_flight", "inflight") + +@Composable +private fun paymentAmountColor(status: String, isOutgoing: Boolean): Color { + val semantic = semanticColors() + return when { + isPendingStatus(status) -> MaterialTheme.colorScheme.onSurfaceVariant // muted + isOutgoing -> MaterialTheme.colorScheme.onSurface // neutral + else -> semantic.success // green + } +} + // ── Single row ─────────────────────────────────────────────────────────────── @Composable @@ -168,16 +182,8 @@ private fun PaymentRow( val semantic = semanticColors() // ── Pending detection ──────────────────────────────────────────────────── - val isPending = payment.status.lowercase() in setOf("pending", "in_flight", "inflight") - - // bitcoin.design: green for received, neutral for sent, muted for pending. - val amountColor = when { - isPending -> MaterialTheme.colorScheme.onSurfaceVariant - payment.isOutgoing -> MaterialTheme.colorScheme.onSurface - else -> semantic.success - } - - val amountPrefix = if (payment.isOutgoing) "−" else "+" + val isPending = isPendingStatus(payment.status) + val amountColor = paymentAmountColor(payment.status, payment.isOutgoing) val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward else Icons.Default.ArrowDownward @@ -235,29 +241,15 @@ private fun PaymentRow( } } Spacer(Modifier.width(12.dp)) - - // Right: sats (primary) + fiat (secondary) — right-aligned - Column(horizontalAlignment = Alignment.End) { - Text( - text = "$amountPrefix${payment.amountSat} sats", - style = MaterialTheme.typography.bodyMedium.copy( - fontFamily = FontFamily.Monospace - ), - color = amountColor - ) - if (fiatLine != null) { - Spacer(Modifier.height(1.dp)) - Text( - text = fiatLine, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f) - ) - } - } + AmountWithFiatColumn( + amountSat = payment.amountSat, + isOutgoing = payment.isOutgoing, + amountColor = amountColor, + fiatLine = fiatLine + ) } } - // ── Detail screen ──────────────────────────────────────────────────────────── @OptIn(ExperimentalMaterial3Api::class) @@ -354,13 +346,8 @@ private fun PaymentDetailContent( fiatSatsPerUnit : Double?, // ← new modifier : Modifier = Modifier ) { - val isPending = payment.status.lowercase() in setOf("pending", "in_flight", "inflight") - val semantic = semanticColors() - val amountColor = when { - isPending -> MaterialTheme.colorScheme.onSurfaceVariant // muted - payment.isOutgoing -> MaterialTheme.colorScheme.onSurface // neutral - else -> semantic.success // green - } + val isPending = isPendingStatus(payment.status) + val amountColor = paymentAmountColor(payment.status, payment.isOutgoing) val context = LocalContext.current val clipboard = LocalClipboard.current @@ -407,24 +394,14 @@ private fun PaymentDetailContent( modifier = Modifier.size(36.dp) ) Spacer(Modifier.height(8.dp)) - - // Primary: sats - Text( - text = "${if (payment.isOutgoing) "−" else "+"}${payment.amountSat} sats", - style = MaterialTheme.typography.headlineLarge, - color = amountColor + AmountWithFiatColumn( + amountSat = payment.amountSat, + isOutgoing = payment.isOutgoing, + amountColor = amountColor, + fiatLine = heroFiat, + style = MaterialTheme.typography.headlineLarge, + horizontalAlignment = Alignment.CenterHorizontally ) - - // Secondary: fiat equivalent - if (heroFiat != null) { - Spacer(Modifier.height(4.dp)) - Text( - text = "≈ $heroFiat", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f) - ) - } - // Fee line (with fiat if available) if (payment.feeSat > 0) { val ppm = feePpm( @@ -433,7 +410,7 @@ private fun PaymentDetailContent( ) val feeLabel = buildString { append("Fee: ${payment.feeSat} sats") - if (feeFiat != null) append(" (≈ $feeFiat)") + if (feeFiat != null) append(" ($feeFiat)") if (ppm != null) append(" · $ppm ppm") } Spacer(Modifier.height(4.dp)) @@ -520,13 +497,7 @@ private fun PaymentDetailContent( } ) } - } - } - - // ── BOLT11 invoice ─────────────────────────────────────────────────── - if (!payment.bolt11.isNullOrBlank()) { - item { - DetailSection(title = "Invoice") { + if (!payment.bolt11.isNullOrBlank()) { DetailRow( label = "BOLT11", value = payment.bolt11, @@ -542,8 +513,6 @@ private fun PaymentDetailContent( } // ── Reusable section wrapper ───────────────────────────────────────────────── -// (unchanged — kept for completeness) - @Composable private fun DetailSection( title : String, @@ -563,8 +532,6 @@ private fun DetailSection( } // ── Single label/value row ─────────────────────────────────────────────────── -// (unchanged) - @Composable private fun DetailRow( label : String, @@ -625,8 +592,6 @@ private fun DetailRow( } // ── Status chip ────────────────────────────────────────────────────────────── -// (unchanged) - @Composable private fun StatusChip(status: String) { val semantic = semanticColors() @@ -675,3 +640,30 @@ private fun formatTimestamp(time: String): String { .format(odt) }.getOrDefault(time) } + +@Composable +private fun AmountWithFiatColumn( + amountSat : Long, + isOutgoing : Boolean, + amountColor : Color, + fiatLine : String?, + style : androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium, + horizontalAlignment: Alignment.Horizontal = Alignment.End +) { + val prefix = if (isOutgoing) "−" else "+" + Column(horizontalAlignment = horizontalAlignment) { + Text( + text = "$prefix$amountSat sats", + style = style.copy(fontFamily = FontFamily.Monospace), + color = amountColor + ) + if (fiatLine != null) { + Spacer(Modifier.height(if (horizontalAlignment == Alignment.CenterHorizontally) 4.dp else 1.dp)) + Text( + text = fiatLine, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt index fd8d996..e0b86c8 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt @@ -3,6 +3,7 @@ package com.bitcointxoko.gudariwallet.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.combinedClickable import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* @@ -10,14 +11,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.ui.draw.alpha import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.filled.ArrowDropDown @@ -29,7 +27,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.platform.LocalFocusManager import com.bitcointxoko.gudariwallet.util.formatFiat -import kotlinx.coroutines.delay +import androidx.compose.ui.text.font.FontFamily @@ -43,18 +41,6 @@ fun HomeTab( val balanceHidden by vm.balanceHidden.collectAsState() - var eyeVisible by remember { mutableStateOf(false) } - LaunchedEffect(balanceHidden) { - eyeVisible = true - delay(1_500) - eyeVisible = false - } - val eyeAlpha by animateFloatAsState( - targetValue = if (eyeVisible) 1f else 0f, - animationSpec = tween(durationMillis = 400), - label = "eyeAlpha" - ) - val isRefreshing = balanceState is BalanceState.Loading || (balanceState as? BalanceState.Success)?.isRefreshing == true @@ -96,9 +82,7 @@ fun HomeTab( } } Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() + verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = if (balanceHidden) @@ -109,14 +93,15 @@ fun HomeTab( tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .size(20.dp) - .alpha(eyeAlpha) ) Spacer(Modifier.width(8.dp)) Text( - text = if (balanceHidden) "••••••" else "${s.sats}", - style = MaterialTheme.typography.displayLarge, + text = if (balanceHidden) "••••••" else formatSats(s.sats), + style = MaterialTheme.typography.displayLarge.copy( + fontFamily = FontFamily.Monospace + ), color = if (balanceHidden) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) else @@ -124,74 +109,93 @@ fun HomeTab( maxLines = 1, softWrap = false, modifier = Modifier - .weight(1f, fill = false) - .clickable( - onClick = { vm.toggleBalanceVisibility() }, - onClickLabel = if (balanceHidden) "Reveal balance" else "Hide balance", + .widthIn(max = 280.dp) + .combinedClickable( + onClick = { if (!balanceHidden) vm.toggleBalanceVisibility() }, + onClickLabel = if (balanceHidden) null else "Hide balance", + onLongClick = { if (balanceHidden) vm.toggleBalanceVisibility() }, + onLongClickLabel = if (balanceHidden) "Reveal balance" else null, indication = null, interactionSource = remember { MutableInteractionSource() } ) ) - Spacer(Modifier.width(8.dp)) - Spacer(Modifier.size(20.dp)) + Text( + text = "sats", + style = MaterialTheme.typography.titleMedium.copy( + fontFamily = FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.width(8.dp)) } - Text( - text = "sats", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) Spacer(Modifier.height(2.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - if (s.fiatAmount != null && s.fiatCurrency != null) { - Text( - text = if (balanceHidden) "≈ ••••" - else "≈ ${formatFiat(s.fiatAmount, s.fiatCurrency)}", - style = MaterialTheme.typography.bodyMedium, - color = if (balanceHidden) - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f) - else - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f), - maxLines = 1, - softWrap = false - ) - Spacer(Modifier.width(2.dp)) - } + val currency = selectedCurrency + if (currency != null) { + // ── Fiat amount + tappable currency code ────────────────────────── Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable( - onClick = { showCurrencyPicker = true }, - onClickLabel = "Change currency", - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) - .padding(start = 0.dp, end = 4.dp, top = 4.dp, bottom = 4.dp) + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center ) { - Text( - text = selectedCurrency ?: "—", - style = MaterialTheme.typography.bodyMedium, - color = if (selectedCurrency != null) - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f) - else - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) - ) - // Triangle only shown when null — signals "tap to set a currency" - if (selectedCurrency == null) { + if (s.fiatAmount != null && s.fiatCurrency != null) { + Text( + text = if (balanceHidden) "••••" + else formatFiat(s.fiatAmount, s.fiatCurrency), + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace + ), + color = if (balanceHidden) + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f) + else + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f), + maxLines = 1, + softWrap = false + ) + Spacer(Modifier.width(4.dp)) + } + // Currency code — always shown when a currency is selected, tappable to change + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + onClick = { showCurrencyPicker = true }, + onClickLabel = "Change currency", + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + .padding(horizontal = 4.dp, vertical = 4.dp) + ) { + Text( + text = currency, + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace + ), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f) + ) Spacer(Modifier.width(2.dp)) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = "Change currency", - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp) ) } } + } else { + // ── No currency selected — ghost prompt ─────────────────────────── + TextButton( + onClick = { showCurrencyPicker = true }, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp) + ) { + Text( + text = "+ Show fiat", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f) + ) + } } + Spacer(Modifier.height(8.dp)) if (!s.isRefreshing) { @@ -231,8 +235,10 @@ fun HomeTab( is BalanceState.Error -> { if (s.staleSats != null) { Text( - text = if (balanceHidden) "••••••" else "${s.staleSats}", - style = MaterialTheme.typography.displayLarge, + text = if (balanceHidden) "••••••" else formatSats(s.staleSats), + style = MaterialTheme.typography.displayLarge.copy( + fontFamily = FontFamily.Monospace + ), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) ) Text( @@ -264,6 +270,9 @@ fun HomeTab( } } +private fun formatSats(sats: Long): String = + "%,d".format(sats).replace(',', ' ') + private fun formatLastUpdated(epochMs: Long): String { val ageMs = System.currentTimeMillis() - epochMs return when {