diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt index ba7d41a..4c58397 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt @@ -1,5 +1,6 @@ package com.bitcointxoko.gudariwallet.ui.contacts +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -44,6 +45,9 @@ fun ContactDetailScreen( val showAddAddressSheet by viewModel.showAddAddressSheet.collectAsStateWithLifecycle() val showEditAliasSheet by viewModel.showEditAliasSheet.collectAsStateWithLifecycle() + var isEditing by remember { mutableStateOf(false) } + var pendingAddressDelete by remember { mutableStateOf(null) } + Scaffold( topBar = { TopAppBar( @@ -68,22 +72,25 @@ fun ContactDetailScreen( }, actions = { if (uiState is ContactDetailUiState.Success) { - IconButton(onClick = { viewModel.openEditAliasSheet() }) { - Icon(Icons.Default.Edit, contentDescription = "Edit name") - } - IconButton(onClick = { viewModel.requestDelete() }) { - Icon( - Icons.Default.Delete, - contentDescription = "Delete contact", - tint = MaterialTheme.colorScheme.error - ) + if (isEditing) { + // Done button exits edit mode + TextButton(onClick = { isEditing = false }) { + Text("Done") + } + } else { + // Edit button opens edit mode + IconButton(onClick = { isEditing = true }) { + Icon(Icons.Default.Edit, contentDescription = "Edit contact") + } } } } ) }, floatingActionButton = { - if (uiState is ContactDetailUiState.Success) { + // FAB only visible in edit mode so users don't accidentally add + // addresses while browsing + if (uiState is ContactDetailUiState.Success && isEditing) { FloatingActionButton(onClick = { viewModel.openAddAddressSheet() }) { Icon(Icons.Default.Add, contentDescription = "Add payment address") } @@ -93,14 +100,18 @@ fun ContactDetailScreen( when (val state = uiState) { is ContactDetailUiState.Loading -> { Box( - Modifier.fillMaxSize().padding(innerPadding), + Modifier + .fillMaxSize() + .padding(innerPadding), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } is ContactDetailUiState.NotFound -> { Box( - Modifier.fillMaxSize().padding(innerPadding), + Modifier + .fillMaxSize() + .padding(innerPadding), contentAlignment = Alignment.Center ) { Text( @@ -113,8 +124,11 @@ fun ContactDetailScreen( is ContactDetailUiState.Success -> { ContactDetailContent( state = state, - onRemoveAddress = { viewModel.removeAddress(it) }, + isEditing = isEditing, + onRemoveAddress = { addressId -> pendingAddressDelete = addressId }, onSetDefault = { viewModel.setDefaultAddress(it) }, + onEditAlias = { viewModel.openEditAliasSheet() }, + onDeleteContact = { viewModel.requestDelete() }, modifier = Modifier.padding(innerPadding) ) } @@ -125,8 +139,8 @@ fun ContactDetailScreen( if (pendingDelete) { AlertDialog( onDismissRequest = { viewModel.cancelDelete() }, - title = { Text("Delete contact") }, - text = { Text("This contact and all their addresses will be permanently removed.") }, + title = { Text("Delete contact") }, + text = { Text("This contact and all their addresses will be permanently removed.") }, confirmButton = { TextButton(onClick = { viewModel.confirmDelete(onDeleted = onBack) }) { Text("Delete", color = MaterialTheme.colorScheme.error) @@ -140,6 +154,27 @@ fun ContactDetailScreen( ) } + pendingAddressDelete?.let { addressId -> + AlertDialog( + onDismissRequest = { pendingAddressDelete = null }, + title = { Text("Remove address") }, + text = { Text("This payment address will be permanently removed from the contact.") }, + confirmButton = { + TextButton(onClick = { + viewModel.removeAddress(addressId) + pendingAddressDelete = null + }) { + Text("Remove", color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { + TextButton(onClick = { pendingAddressDelete = null }) { + Text(strings.cancel) + } + } + ) + } + // ── Add address sheet ───────────────────────────────────────────────── if (showAddAddressSheet) { AddAddressSheet( @@ -171,8 +206,11 @@ fun ContactDetailScreen( @Composable private fun ContactDetailContent( state: ContactDetailUiState.Success, + isEditing: Boolean, onRemoveAddress: (addressId: String) -> Unit, onSetDefault: (PaymentAddressEntity) -> Unit, + onEditAlias: () -> Unit, + onDeleteContact: () -> Unit, modifier: Modifier = Modifier ) { val contact = state.data.contact @@ -225,29 +263,31 @@ private fun ContactDetailContent( // ── Payment addresses section ───────────────────────────────────── item { - DetailSection(title = "Payment addresses") {} - } - - if (addresses.isEmpty()) { - item { - Text( - text = "No addresses added yet", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - } - } else { - items(addresses, key = { it.id }) { addr -> - AddressRow( - address = addr, - onRemove = { onRemoveAddress(addr.id) }, - onSetDefault = { onSetDefault(addr) } - ) - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - thickness = 0.5.dp - ) + DetailSection(title = "Payment addresses") { + if (addresses.isEmpty()) { + Text( + text = "No addresses added yet", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + Column(verticalArrangement = Arrangement.spacedBy(0.dp)) { + addresses.forEachIndexed { index, addr -> + AddressRow( + address = addr, + isEditing = isEditing, + onRemove = { onRemoveAddress(addr.id) }, + onSetDefault = { onSetDefault(addr) } + ) + if (index < addresses.lastIndex) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 0.5.dp + ) + } + } + } + } } } @@ -259,6 +299,47 @@ private fun ContactDetailContent( } } } + + // ── Destructive actions (edit mode only) ───────────────────────── + if (isEditing) { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedButton( + onClick = onEditAlias, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.width(8.dp)) + Text("Edit name") + } + OutlinedButton( + onClick = onDeleteContact, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error) + ) { + Icon( + Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.width(8.dp)) + Text("Delete contact") + } + } + } + } } } @@ -267,8 +348,9 @@ private fun ContactDetailContent( @Composable private fun AddressRow( address: PaymentAddressEntity, + isEditing: Boolean, onRemove: () -> Unit, - onSetDefault: () -> Unit + onSetDefault: () -> Unit, ) { val typeLabel = when (address.type) { PaymentAddressType.LIGHTNING_ADDRESS.name -> "Lightning Address" @@ -279,46 +361,95 @@ private fun AddressRow( PaymentAddressType.BIP353.name -> "BIP-353" else -> address.type } - val label = address.label?.let { "$typeLabel · $it" } ?: typeLabel - DetailRow( - label = label, - value = address.address, - maxLines = 1, - trailingContent = { - Row { - // Set default star — filled if already default + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // ── Label + value ───────────────────────────────────────────── + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = typeLabel, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + address.label?.let { lbl -> + Text( + text = "· $lbl", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (address.isDefault) { + SuggestionChip( + onClick = {}, + label = { + Text( + "default", + style = MaterialTheme.typography.labelSmall + ) + }, + modifier = Modifier.height(20.dp) + ) + } + } + Text( + text = address.address, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + // ── Trailing actions ────────────────────────────────────────── + Row(horizontalArrangement = Arrangement.spacedBy(0.dp)) { + // Copy always available + CopyIconButton( + label = "Copy address", + value = address.address, + size = 36.dp + ) + // Star + delete only in edit mode + if (isEditing) { IconButton( onClick = onSetDefault, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(36.dp) ) { Icon( imageVector = if (address.isDefault) Icons.Filled.Star else Icons.Outlined.Star, contentDescription = if (address.isDefault) "Default address" else "Set as default", - modifier = Modifier.size(16.dp), + modifier = Modifier.size(18.dp), tint = if (address.isDefault) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) } - // Remove IconButton( onClick = onRemove, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(36.dp) ) { Icon( imageVector = Icons.Default.Delete, contentDescription = "Remove address", - modifier = Modifier.size(16.dp), + modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.error ) } } } - ) + } } + + + // ── Add address sheet ────────────────────────────────────────────────────────── @OptIn(ExperimentalMaterial3Api::class)