feat(contacts): P2 шаг1 — типы TEL/EMAIL (парсинг из raw, подписи в карточке, сохранение при правке) +4 теста

This commit is contained in:
b-mob-contacts
2026-07-13 08:57:34 +00:00
parent 559a35aeb3
commit a8741e982c
6 changed files with 151 additions and 26 deletions
@@ -180,31 +180,27 @@ private fun ContactDetailContent(
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(0.dp),
) {
if (contact.emailLines.isNotEmpty()) {
contact.emailLines.forEachIndexed { index, email ->
ContactDetailField(
label = if (index == 0) "Email" else "Email ${index + 1}",
value = email,
onClick = {
context.startActivity(
Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$email")),
)
},
)
}
contact.emailEntries.forEachIndexed { index, entry ->
ContactDetailField(
label = ContactUi.emailTypeLabel(entry.type, index),
value = entry.value,
onClick = {
context.startActivity(
Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:${entry.value}")),
)
},
)
}
if (contact.phoneLines.isNotEmpty()) {
contact.phoneLines.forEachIndexed { index, phone ->
ContactDetailField(
label = if (index == 0) "Телефон" else "Телефон ${index + 1}",
value = phone,
onClick = {
context.startActivity(
Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phone")),
)
},
)
}
contact.phoneEntries.forEachIndexed { index, entry ->
ContactDetailField(
label = ContactUi.phoneTypeLabel(entry.type, index),
value = entry.value,
onClick = {
context.startActivity(
Intent(Intent.ACTION_DIAL, Uri.parse("tel:${entry.value}")),
)
},
)
}
if (contact.address.isNotBlank()) {
ContactDetailField(label = "Адрес", value = contact.address)
@@ -372,4 +368,21 @@ internal object ContactUi {
android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
}.getOrNull()?.takeIf { it.isNotEmpty() }
}
// P2: подпись типа EMAIL/TEL по токену TYPE (WORK/HOME/CELL/…); index — фолбэк-нумерация.
fun emailTypeLabel(type: String, index: Int): String = when (type.uppercase()) {
"WORK" -> "Рабочий"
"HOME" -> "Личный"
"" -> if (index == 0) "Email" else "Email ${index + 1}"
else -> type.lowercase().replaceFirstChar { it.uppercase() }
}
fun phoneTypeLabel(type: String, index: Int): String = when (type.uppercase()) {
"CELL", "MOBILE" -> "Мобильный"
"WORK" -> "Рабочий"
"HOME" -> "Домашний"
"FAX" -> "Факс"
"" -> if (index == 0) "Телефон" else "Телефон ${index + 1}"
else -> type.lowercase().replaceFirstChar { it.uppercase() }
}
}
@@ -10,6 +10,7 @@ import ru.forbion.f7cloud.core.database.F7Database
import ru.forbion.f7cloud.core.network.CardDavClient
import ru.forbion.f7cloud.core.network.NetworkFactory
import ru.forbion.f7cloud.core.network.VCardFields
import ru.forbion.f7cloud.core.network.VCardTypedValue
data class ContactItem(
val uid: String,
@@ -45,6 +46,21 @@ data class ContactItem(
.filter { it.isNotBlank() }
.distinct()
// P2: значения с типом (WORK/HOME/CELL) из сырого vCard; фолбэк на значения без типа.
val emailEntries: List<VCardTypedValue>
get() = if (raw.isNotBlank()) {
CardDavClient.parseTypedValues(raw, "EMAIL")
} else {
emailLines.map { VCardTypedValue(it) }
}
val phoneEntries: List<VCardTypedValue>
get() = if (raw.isNotBlank()) {
CardDavClient.parseTypedValues(raw, "TEL")
} else {
phoneLines.map { VCardTypedValue(it) }
}
val subtitle: String?
get() = when {
title.isNotBlank() && organization.isNotBlank() -> "$title · $organization"
@@ -169,12 +169,16 @@ private fun EditContactDialog(
var website by remember(contact.uid) { mutableStateOf(contact.website) }
var birthday by remember(contact.uid) { mutableStateOf(contact.birthday) }
// Типы сохраняем по значению: неизменённые адреса/номера не теряют TYPE (fix P1-каветата).
val emailTypeByValue = contact.emailEntries.associate { it.value to it.type }
val phoneTypeByValue = contact.phoneEntries.associate { it.value to it.type }
fun buildFields(): VCardFields {
fun lines(v: String) = v.lines().map { it.trim() }.filter { it.isNotBlank() }
return VCardFields(
displayName = name.trim(),
emails = lines(emails).map { VCardTypedValue(it) },
phones = lines(phones).map { VCardTypedValue(it) },
emails = lines(emails).map { VCardTypedValue(it, emailTypeByValue[it].orEmpty()) },
phones = lines(phones).map { VCardTypedValue(it, phoneTypeByValue[it].orEmpty()) },
organization = organization.trim(),
title = title.trim(),
address = address.trim(),