Improved showing of suggestions on MainScreen

This commit is contained in:
Juha Heinanen
2026-01-18 22:44:45 +02:00
parent f28dbefbd9
commit c5bd9bdd80
3 changed files with 77 additions and 47 deletions

View File

@ -255,7 +255,7 @@ private fun ContactsContent(
Utils.unaccent(contact.name()).contains(normalizedQuery, ignoreCase = true) Utils.unaccent(contact.name()).contains(normalizedQuery, ignoreCase = true)
} }
.map { contact -> .map { contact ->
Pair(contact, buildAnnotatedStringWithHighlight(contact.name(), searchQuery)) Pair(contact, Utils.buildAnnotatedStringWithHighlight(contact.name(), searchQuery))
} }
} }
} }
@ -432,20 +432,3 @@ 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))
}
}
}

View File

@ -128,6 +128,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -483,7 +484,9 @@ private fun MainScreen(
} }
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().imePadding(), modifier = Modifier
.fillMaxSize()
.imePadding(),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
Column( Column(
@ -652,8 +655,7 @@ private fun TopAppBar(
onClick = { onClick = {
if (Build.VERSION.SDK_INT >= 31) if (Build.VERSION.SDK_INT >= 31)
Log.d(TAG, "Toggling speakerphone when dev/mode is " + Log.d(TAG, "Toggling speakerphone when dev/mode is " +
"${am.communicationDevice!!.type}/${am.mode}" "${am.communicationDevice!!.type}/${am.mode}")
)
isSpeakerOn.value = !Utils.isSpeakerPhoneOn(am) isSpeakerOn.value = !Utils.isSpeakerPhoneOn(am)
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
}, },
@ -826,7 +828,9 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
IconButton( IconButton(
onClick = { viewModel.toggleDialpadVisibility() }, onClick = { viewModel.toggleDialpadVisibility() },
modifier = Modifier.weight(1f).size(buttonSize), modifier = Modifier
.weight(1f)
.size(buttonSize),
enabled = dialpadButtonEnabled.value enabled = dialpadButtonEnabled.value
) { ) {
Icon( Icon(
@ -1060,8 +1064,7 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
if (Api.account_regint(acc.accp) > 0) { if (Api.account_regint(acc.accp) > 0) {
Api.account_set_regint(acc.accp, 0) Api.account_set_regint(acc.accp, 0)
Api.ua_unregister(ua.uap) Api.ua_unregister(ua.uap)
} } else {
else {
Api.account_set_regint( Api.account_set_regint(
acc.accp, acc.accp,
acc.configuredRegInt acc.configuredRegInt
@ -1112,8 +1115,7 @@ private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: Na
if (Api.account_regint(acc.accp) > 0) { if (Api.account_regint(acc.accp) > 0) {
Api.account_set_regint(acc.accp, 0) Api.account_set_regint(acc.accp, 0)
Api.ua_unregister(ua.uap) Api.ua_unregister(ua.uap)
} } else {
else {
Api.account_set_regint( Api.account_set_regint(
acc.accp, acc.accp,
acc.configuredRegInt acc.configuredRegInt
@ -1179,15 +1181,17 @@ private fun CallUriRow(
val isDialer = dialerState != null val isDialer = dialerState != null
val suggestions by remember { contactNames } val suggestions by remember { contactNames }
var filteredSuggestions by remember { mutableStateOf(suggestions) } var filteredSuggestions by remember { mutableStateOf<List<AnnotatedString>>(emptyList()) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val isDialpadVisible by viewModel.isDialpadVisible.collectAsState() val isDialpadVisible by viewModel.isDialpadVisible.collectAsState()
Row(modifier = Modifier Row(
.fillMaxWidth() modifier = Modifier
.padding(top = 4.dp, bottom = 8.dp), .fillMaxWidth()
verticalAlignment = Alignment.CenterVertically) { .padding(top = 4.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@ -1200,10 +1204,21 @@ private fun CallUriRow(
if (isDialer) { if (isDialer) {
if (it != dialerState.callUri.value) { if (it != dialerState.callUri.value) {
dialerState.callUri.value = it dialerState.callUri.value = it
filteredSuggestions = suggestions.filter { suggestion -> if (it == "") {
it.length > 2 && suggestion.startsWith(it, ignoreCase = true) dialerState.showCallButton.value = true
dialerState.showCallConferenceButton.value = true
} }
dialerState.showSuggestions.value = it.length > 2 val normalizedInput = Utils.unaccent(it)
filteredSuggestions = suggestions
.filter { suggestion ->
it.length > 1 &&
Utils.unaccent(suggestion)
.contains(normalizedInput, ignoreCase = true)
}
.map { suggestion ->
Utils.buildAnnotatedStringWithHighlight(suggestion, it)
}
dialerState.showSuggestions.value = it.length > 1
} }
} }
}, },
@ -1214,8 +1229,9 @@ private fun CallUriRow(
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (dialerState.showSuggestions.value) if (dialerState.showSuggestions.value)
dialerState.showSuggestions.value = false dialerState.showSuggestions.value = false
else dialerState.callUri.value = ""
dialerState.callUri.value = "" dialerState.showCallButton.value = true
dialerState.showCallConferenceButton.value = true
}, },
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -1233,10 +1249,11 @@ private fun CallUriRow(
} }
}, },
label = { label = {
Text(text = if (isDialer) Text(
dialerState.callUriLabel.value text = if (isDialer)
else dialerState.callUriLabel.value
call!!.callUriLabel.value, else
call!!.callUriLabel.value,
fontSize = 18.sp fontSize = 18.sp
) )
}, },
@ -1258,9 +1275,11 @@ private fun CallUriRow(
.animateContentSize() .animateContentSize()
) { ) {
if (isDialer && dialerState.showSuggestions.value && filteredSuggestions.isNotEmpty()) { if (isDialer && dialerState.showSuggestions.value && filteredSuggestions.isNotEmpty()) {
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.heightIn(max = 150.dp)) { .fillMaxWidth()
.heightIn(max = 150.dp)
) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -1273,13 +1292,13 @@ private fun CallUriRow(
) { ) {
items( items(
items = filteredSuggestions, items = filteredSuggestions,
key = { suggestion -> suggestion } key = { suggestion -> suggestion.toString() }
) { suggestion -> ) { suggestion ->
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
dialerState.callUri.value = suggestion dialerState.callUri.value = suggestion.toString()
dialerState.showSuggestions.value = false dialerState.showSuggestions.value = false
} }
.padding(12.dp) .padding(12.dp)
@ -1619,7 +1638,12 @@ private fun CallRow(
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp) .padding(
start = 4.dp,
end = 4.dp,
top = 12.dp,
bottom = 2.dp
)
.focusRequester(focusRequester), .focusRequester(focusRequester),
label = { Text(stringResource(R.string.transfer_destination)) }, label = { Text(stringResource(R.string.transfer_destination)) },
textStyle = TextStyle(fontSize = 18.sp), textStyle = TextStyle(fontSize = 18.sp),
@ -1662,7 +1686,8 @@ private fun CallRow(
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
transferUri = suggestion transferUri = suggestion
call.showSuggestions.value = false call.showSuggestions.value =
false
} }
.padding(12.dp) .padding(12.dp)
) { ) {

View File

@ -29,6 +29,11 @@ import android.text.format.DateUtils
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.core.net.toUri import androidx.core.net.toUri
@ -424,6 +429,23 @@ object Utils {
return "\\p{InCombiningDiacriticalMarks}+".toRegex().replace(normalized, "") return "\\p{InCombiningDiacriticalMarks}+".toRegex().replace(normalized, "")
} }
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))
}
}
}
fun isVisible(): Boolean { fun isVisible(): Boolean {
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
} }