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) {
|
suspend fun removeAddress(addressId: String) {
|
||||||
dao.deleteAddressById(addressId)
|
dao.deleteAddressById(addressId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,24 @@ interface ContactDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertAddress(address: PaymentAddressEntity)
|
suspend fun insertAddress(address: PaymentAddressEntity)
|
||||||
|
|
||||||
@Update
|
@Query("""
|
||||||
suspend fun updateAddress(address: PaymentAddressEntity)
|
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")
|
@Query("DELETE FROM payment_addresses WHERE id = :id")
|
||||||
suspend fun deleteAddressById(id: String)
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -49,6 +48,7 @@ fun ContactDetailScreen(
|
|||||||
|
|
||||||
var isEditing by remember { mutableStateOf(false) }
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
var pendingAddressDelete by remember { mutableStateOf<String?>(null) }
|
var pendingAddressDelete by remember { mutableStateOf<String?>(null) }
|
||||||
|
var editingAddress by remember { mutableStateOf<PaymentAddressEntity?>(null) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -131,8 +131,9 @@ fun ContactDetailScreen(
|
|||||||
onSetDefault = { viewModel.setDefaultAddress(it) },
|
onSetDefault = { viewModel.setDefaultAddress(it) },
|
||||||
onPayAddress = onPayAddress,
|
onPayAddress = onPayAddress,
|
||||||
onEditAlias = { viewModel.openEditAliasSheet() },
|
onEditAlias = { viewModel.openEditAliasSheet() },
|
||||||
|
onEditAddress = { addr -> editingAddress = addr },
|
||||||
onDeleteContact = { viewModel.requestDelete() },
|
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 ──────────────────────────────────────────────────
|
// ── Edit alias sheet ──────────────────────────────────────────────────
|
||||||
if (showEditAliasSheet) {
|
if (showEditAliasSheet) {
|
||||||
val currentAlias = (uiState as? ContactDetailUiState.Success)
|
val currentAlias = (uiState as? ContactDetailUiState.Success)
|
||||||
@@ -214,6 +227,7 @@ private fun ContactDetailContent(
|
|||||||
onSetDefault: (PaymentAddressEntity) -> Unit,
|
onSetDefault: (PaymentAddressEntity) -> Unit,
|
||||||
onPayAddress: (address: String) -> Unit,
|
onPayAddress: (address: String) -> Unit,
|
||||||
onEditAlias: () -> Unit,
|
onEditAlias: () -> Unit,
|
||||||
|
onEditAddress: (PaymentAddressEntity) -> Unit,
|
||||||
onDeleteContact: () -> Unit,
|
onDeleteContact: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -282,7 +296,8 @@ private fun ContactDetailContent(
|
|||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
onPay = { onPayAddress(addr.address) },
|
onPay = { onPayAddress(addr.address) },
|
||||||
onRemove = { onRemoveAddress(addr.id) },
|
onRemove = { onRemoveAddress(addr.id) },
|
||||||
onSetDefault = { onSetDefault(addr) }
|
onSetDefault = { onSetDefault(addr) },
|
||||||
|
onEdit = { onEditAddress(addr) }
|
||||||
)
|
)
|
||||||
if (index < addresses.lastIndex) {
|
if (index < addresses.lastIndex) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -357,6 +372,7 @@ private fun AddressRow(
|
|||||||
onPay: () -> Unit,
|
onPay: () -> Unit,
|
||||||
onRemove: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
onSetDefault: () -> Unit,
|
onSetDefault: () -> Unit,
|
||||||
|
onEdit: () -> Unit
|
||||||
) {
|
) {
|
||||||
val typeLabel = when (address.type) {
|
val typeLabel = when (address.type) {
|
||||||
PaymentAddressType.LIGHTNING_ADDRESS.name -> "Lightning Address"
|
PaymentAddressType.LIGHTNING_ADDRESS.name -> "Lightning Address"
|
||||||
@@ -380,6 +396,13 @@ private fun AddressRow(
|
|||||||
) {
|
) {
|
||||||
// ── Label + value ─────────────────────────────────────────────
|
// ── Label + value ─────────────────────────────────────────────
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = address.address,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
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 ──────────────────────────────────────────
|
// ── Trailing actions ──────────────────────────────────────────
|
||||||
@@ -428,17 +445,14 @@ private fun AddressRow(
|
|||||||
// Star + delete only in edit mode
|
// Star + delete only in edit mode
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onSetDefault,
|
onClick = onEdit,
|
||||||
modifier = Modifier.size(36.dp)
|
modifier = Modifier.size(36.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (address.isDefault) Icons.Filled.Star
|
imageVector = Icons.Default.Edit,
|
||||||
else Icons.Outlined.Star,
|
contentDescription = "Edit address",
|
||||||
contentDescription = if (address.isDefault) "Default address"
|
|
||||||
else "Set as default",
|
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = if (address.isDefault) MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -458,8 +472,6 @@ private fun AddressRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Add address sheet ──────────────────────────────────────────────────────────
|
// ── Add address sheet ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@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 ───────────────────────────────────────────────────────────
|
// ── Edit alias sheet ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@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) {
|
fun removeAddress(addressId: String) {
|
||||||
viewModelScope.launch { repo.removeAddress(addressId) }
|
viewModelScope.launch { repo.removeAddress(addressId) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitcointxoko.gudariwallet.data.ContactRepository
|
import com.bitcointxoko.gudariwallet.data.ContactRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
||||||
|
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressEntity
|
||||||
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -50,6 +51,7 @@ class ContactsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Factory(private val repo: ContactRepository) : ViewModelProvider.Factory {
|
class Factory(private val repo: ContactRepository) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
|
|||||||
Reference in New Issue
Block a user