contact details ui
This commit is contained in:
@@ -104,6 +104,35 @@ class ContactRepository(app: Application) {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateAddress(
|
||||
addressId: String,
|
||||
type: PaymentAddressType,
|
||||
address: String,
|
||||
label: String?,
|
||||
makeDefault: Boolean
|
||||
) {
|
||||
val existing = dao.getAddressById(addressId) ?: return
|
||||
val contactId = existing.contactId
|
||||
|
||||
if (makeDefault) dao.clearDefault(contactId, type.name)
|
||||
|
||||
// If LIGHTNING_ADDRESS and default, also sync the fast-lookup column
|
||||
if (type == PaymentAddressType.LIGHTNING_ADDRESS && makeDefault) {
|
||||
dao.getById(contactId)?.let {
|
||||
dao.update(it.copy(lnAddress = address, updatedAt = System.currentTimeMillis()))
|
||||
}
|
||||
}
|
||||
|
||||
dao.updateAddress(
|
||||
addressId = addressId,
|
||||
type = type.name,
|
||||
address = address,
|
||||
label = label,
|
||||
isDefault = makeDefault
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
suspend fun removeAddress(addressId: String) {
|
||||
dao.deleteAddressById(addressId)
|
||||
}
|
||||
|
||||
@@ -54,8 +54,24 @@ interface ContactDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAddress(address: PaymentAddressEntity)
|
||||
|
||||
@Update
|
||||
suspend fun updateAddress(address: PaymentAddressEntity)
|
||||
@Query("""
|
||||
UPDATE payment_addresses
|
||||
SET type = :type,
|
||||
address = :address,
|
||||
label = :label,
|
||||
isDefault = :isDefault
|
||||
WHERE id = :addressId
|
||||
""")
|
||||
suspend fun updateAddress(
|
||||
addressId: String,
|
||||
type: String,
|
||||
address: String,
|
||||
label: String?,
|
||||
isDefault: Boolean
|
||||
)
|
||||
|
||||
@Query("SELECT * FROM payment_addresses WHERE id = :addressId")
|
||||
suspend fun getAddressById(addressId: String): PaymentAddressEntity?
|
||||
|
||||
@Query("DELETE FROM payment_addresses WHERE id = :id")
|
||||
suspend fun deleteAddressById(id: String)
|
||||
|
||||
+160
-18
@@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -49,6 +48,7 @@ fun ContactDetailScreen(
|
||||
|
||||
var isEditing by remember { mutableStateOf(false) }
|
||||
var pendingAddressDelete by remember { mutableStateOf<String?>(null) }
|
||||
var editingAddress by remember { mutableStateOf<PaymentAddressEntity?>(null) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -131,8 +131,9 @@ fun ContactDetailScreen(
|
||||
onSetDefault = { viewModel.setDefaultAddress(it) },
|
||||
onPayAddress = onPayAddress,
|
||||
onEditAlias = { viewModel.openEditAliasSheet() },
|
||||
onEditAddress = { addr -> editingAddress = addr },
|
||||
onDeleteContact = { viewModel.requestDelete() },
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
modifier = Modifier.padding(innerPadding).padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -189,6 +190,18 @@ fun ContactDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// ── Edit address sheet ──────────────────────────────────────────
|
||||
editingAddress?.let { addr ->
|
||||
EditAddressSheet(
|
||||
address = addr,
|
||||
onSave = { id, type, addressValue, label, makeDefault ->
|
||||
viewModel.updateAddress(id, type, addressValue, label, makeDefault)
|
||||
editingAddress = null
|
||||
},
|
||||
onDismiss = { editingAddress = null }
|
||||
)
|
||||
}
|
||||
|
||||
// ── Edit alias sheet ──────────────────────────────────────────────────
|
||||
if (showEditAliasSheet) {
|
||||
val currentAlias = (uiState as? ContactDetailUiState.Success)
|
||||
@@ -214,6 +227,7 @@ private fun ContactDetailContent(
|
||||
onSetDefault: (PaymentAddressEntity) -> Unit,
|
||||
onPayAddress: (address: String) -> Unit,
|
||||
onEditAlias: () -> Unit,
|
||||
onEditAddress: (PaymentAddressEntity) -> Unit,
|
||||
onDeleteContact: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -282,7 +296,8 @@ private fun ContactDetailContent(
|
||||
isEditing = isEditing,
|
||||
onPay = { onPayAddress(addr.address) },
|
||||
onRemove = { onRemoveAddress(addr.id) },
|
||||
onSetDefault = { onSetDefault(addr) }
|
||||
onSetDefault = { onSetDefault(addr) },
|
||||
onEdit = { onEditAddress(addr) }
|
||||
)
|
||||
if (index < addresses.lastIndex) {
|
||||
HorizontalDivider(
|
||||
@@ -357,6 +372,7 @@ private fun AddressRow(
|
||||
onPay: () -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
onSetDefault: () -> Unit,
|
||||
onEdit: () -> Unit
|
||||
) {
|
||||
val typeLabel = when (address.type) {
|
||||
PaymentAddressType.LIGHTNING_ADDRESS.name -> "Lightning Address"
|
||||
@@ -380,6 +396,13 @@ private fun AddressRow(
|
||||
) {
|
||||
// ── Label + value ─────────────────────────────────────────────
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = address.address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
@@ -409,12 +432,6 @@ private fun AddressRow(
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = address.address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
// ── Trailing actions ──────────────────────────────────────────
|
||||
@@ -428,17 +445,14 @@ private fun AddressRow(
|
||||
// Star + delete only in edit mode
|
||||
if (isEditing) {
|
||||
IconButton(
|
||||
onClick = onSetDefault,
|
||||
onClick = onEdit,
|
||||
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",
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Edit address",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (address.isDefault) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
@@ -458,8 +472,6 @@ private fun AddressRow(
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ── Add address sheet ──────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -585,6 +597,136 @@ private fun AddAddressSheet(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit address sheet ────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun EditAddressSheet(
|
||||
address: PaymentAddressEntity,
|
||||
onSave: (id: String, type: PaymentAddressType, addressValue: String, label: String?, makeDefault: Boolean) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val focusManager = LocalFocusManager.current
|
||||
val addressFocus = remember { FocusRequester() }
|
||||
|
||||
var selectedType by remember {
|
||||
mutableStateOf(PaymentAddressType.entries.find { it.name == address.type }
|
||||
?: PaymentAddressType.LIGHTNING_ADDRESS)
|
||||
}
|
||||
var addressValue by remember { mutableStateOf(address.address) }
|
||||
var label by remember { mutableStateOf(address.label ?: "") }
|
||||
var makeDefault by remember { mutableStateOf(address.isDefault) }
|
||||
|
||||
val canSave = addressValue.isNotBlank()
|
||||
|
||||
LaunchedEffect(Unit) { addressFocus.requestFocus() }
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Edit payment address", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
// Type chips
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FilterChip(
|
||||
selected = selectedType == PaymentAddressType.LIGHTNING_ADDRESS,
|
||||
onClick = { selectedType = PaymentAddressType.LIGHTNING_ADDRESS },
|
||||
label = { Text("Lightning Address") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedType == PaymentAddressType.LNURL,
|
||||
onClick = { selectedType = PaymentAddressType.LNURL },
|
||||
label = { Text("LNURL") }
|
||||
)
|
||||
}
|
||||
|
||||
// Address field
|
||||
val addressLabel = if (selectedType == PaymentAddressType.LIGHTNING_ADDRESS)
|
||||
"Lightning Address" else "LNURL"
|
||||
val addressPlaceholder = if (selectedType == PaymentAddressType.LIGHTNING_ADDRESS)
|
||||
"user@domain.com" else "LNURL1..."
|
||||
|
||||
OutlinedTextField(
|
||||
value = addressValue,
|
||||
onValueChange = { addressValue = it },
|
||||
label = { Text(addressLabel) },
|
||||
placeholder = { Text(addressPlaceholder) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next,
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(addressFocus)
|
||||
)
|
||||
|
||||
// Optional label
|
||||
OutlinedTextField(
|
||||
value = label,
|
||||
onValueChange = { label = it },
|
||||
label = { Text("Label (optional)") },
|
||||
placeholder = { Text("e.g. personal, savings") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Words,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focusManager.clearFocus()
|
||||
if (canSave) onSave(address.id, selectedType, addressValue, label.takeIf { it.isNotBlank() }, makeDefault)
|
||||
}
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Set as default toggle
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Set as default",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Switch(
|
||||
checked = makeDefault,
|
||||
onCheckedChange = { makeDefault = it }
|
||||
)
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)
|
||||
) {
|
||||
val strings = LocalAppStrings.current
|
||||
TextButton(onClick = onDismiss) { Text(strings.cancel) }
|
||||
Button(
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
onSave(address.id, selectedType, addressValue, label.takeIf { it.isNotBlank() }, makeDefault)
|
||||
},
|
||||
enabled = canSave
|
||||
) { Text("Save") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Edit alias sheet ───────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@@ -68,6 +68,19 @@ class ContactDetailViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAddress(
|
||||
addressId: String,
|
||||
type: PaymentAddressType,
|
||||
address: String,
|
||||
label: String?,
|
||||
makeDefault: Boolean
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
repo.updateAddress(addressId, type, address, label, makeDefault)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeAddress(addressId: String) {
|
||||
viewModelScope.launch { repo.removeAddress(addressId) }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitcointxoko.gudariwallet.data.ContactRepository
|
||||
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
||||
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressEntity
|
||||
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -50,6 +51,7 @@ class ContactsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Factory(private val repo: ContactRepository) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
|
||||
Reference in New Issue
Block a user