In contact search, highlight matched characters

This commit is contained in:
Juha Heinanen
2026-01-17 05:06:32 +02:00
parent 3f7a60cb27
commit 161735baf8
@@ -53,11 +53,15 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -240,13 +244,19 @@ private fun ContactsContent(
} }
val filteredContacts = remember(BaresipService.contacts, searchQuery) { val filteredContacts = remember(BaresipService.contacts, searchQuery) {
if (searchQuery.isBlank()) if (searchQuery.isBlank()) {
BaresipService.contacts BaresipService.contacts.map { contact ->
else { Pair(contact, buildAnnotatedString { append(contact.name()) })
}
} else {
val normalizedQuery = Utils.unaccent(searchQuery) val normalizedQuery = Utils.unaccent(searchQuery)
BaresipService.contacts.filter { contact -> BaresipService.contacts
.filter { contact ->
Utils.unaccent(contact.name()).contains(normalizedQuery, ignoreCase = true) Utils.unaccent(contact.name()).contains(normalizedQuery, ignoreCase = true)
} }
.map { contact ->
Pair(contact, buildAnnotatedStringWithHighlight(contact.name(), searchQuery))
}
} }
} }
@@ -264,9 +274,7 @@ private fun ContactsContent(
state = lazyListState, state = lazyListState,
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
items(filteredContacts, key = { it.id() }) { contact -> items(filteredContacts, key = { it.first.id() }) { (contact, annotatedName) ->
val name = contact.name()
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -286,7 +294,7 @@ private fun ContactsContent(
.clip(CircleShape) .clip(CircleShape)
) )
else else
TextAvatar(name, contact.color) TextAvatar(contact.name(), contact.color)
} }
is Contact.AndroidContact -> { is Contact.AndroidContact -> {
@@ -301,14 +309,14 @@ private fun ContactsContent(
.clip(CircleShape), .clip(CircleShape),
) )
else else
TextAvatar(name, contact.color) TextAvatar(contact.name(), contact.color)
} }
} }
when (contact) { when (contact) {
is Contact.BaresipContact -> { is Contact.BaresipContact -> {
Text(text = name, Text(text = annotatedName,
fontSize = 20.sp, fontSize = 20.sp,
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal, fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
modifier = Modifier modifier = Modifier
@@ -327,7 +335,7 @@ private fun ContactsContent(
Log.w(TAG, "onClickListener did not find UA for $aor") Log.w(TAG, "onClickListener did not find UA for $aor")
dialogMessage.value = String.format( dialogMessage.value = String.format(
ctx.getString(R.string.contact_action_question), ctx.getString(R.string.contact_action_question),
name contact.name()
) )
positiveText.value = ctx.getString(R.string.call) positiveText.value = ctx.getString(R.string.call)
positiveAction.value = { positiveAction.value = {
@@ -351,7 +359,7 @@ private fun ContactsContent(
onLongClick = { onLongClick = {
dialogMessage.value = String.format( dialogMessage.value = String.format(
ctx.getString(R.string.contact_delete_question), ctx.getString(R.string.contact_delete_question),
name contact.name()
) )
positiveText.value = ctx.getString(R.string.delete) positiveText.value = ctx.getString(R.string.delete)
positiveAction.value = { positiveAction.value = {
@@ -379,7 +387,7 @@ private fun ContactsContent(
) )
SmallFloatingActionButton( SmallFloatingActionButton(
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
onClick = { navController.navigate("baresip_contact/${name}/old") }, onClick = { navController.navigate("baresip_contact/${contact.name()}/old") },
containerColor = MaterialTheme.colorScheme.tertiaryContainer, containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer contentColor = MaterialTheme.colorScheme.onTertiaryContainer
) { ) {
@@ -392,24 +400,24 @@ private fun ContactsContent(
} }
is Contact.AndroidContact -> { is Contact.AndroidContact -> {
Text(text = name, Text(text = annotatedName,
fontSize = 20.sp, fontSize = 20.sp,
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal, fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 10.dp, top = 4.dp, bottom = 4.dp) .padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
.combinedClickable( .combinedClickable(
onClick = { navController.navigate("android_contact/${name}") }, onClick = { navController.navigate("android_contact/${contact.name()}") },
onLongClick = { onLongClick = {
dialogMessage.value = String.format( dialogMessage.value = String.format(
ctx.getString(R.string.contact_delete_question), ctx.getString(R.string.contact_delete_question),
name contact.name()
) )
positiveText.value = ctx.getString(R.string.delete) positiveText.value = ctx.getString(R.string.delete)
positiveAction.value = { positiveAction.value = {
ctx.contentResolver.delete( ctx.contentResolver.delete(
ContactsContract.RawContacts.CONTENT_URI, ContactsContract.RawContacts.CONTENT_URI,
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'", ContactsContract.Contacts.DISPLAY_NAME + "='" + contact.name() + "'",
null null
) )
} }
@@ -424,3 +432,20 @@ private fun ContactsContent(
} }
} }
} }
private fun buildAnnotatedStringWithHighlight(name: String, query: String): AnnotatedString {
val normalizedName = Utils.unaccent(name)
val normalizedQuery = Utils.unaccent(query)
val startIndex = normalizedName.indexOf(normalizedQuery, ignoreCase = true)
return if (startIndex == -1) {
buildAnnotatedString { append(name) }
} else {
buildAnnotatedString {
append(name.take(startIndex))
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(name.drop(startIndex).take(normalizedQuery.length))
}
append(name.drop(startIndex + normalizedQuery.length))
}
}
}