enhancement: search contacts
This commit is contained in:
@@ -7,17 +7,26 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
||||
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -29,6 +38,8 @@ fun ContactsScreen(
|
||||
val strings = LocalAppStrings.current
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var showCreateSheet by remember { mutableStateOf(false) }
|
||||
var searchVisible by rememberSaveable { mutableStateOf(false) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -41,6 +52,17 @@ fun ContactsScreen(
|
||||
contentDescription = strings.back
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
searchVisible = !searchVisible
|
||||
if (!searchVisible) searchQuery = ""
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search contacts"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -50,12 +72,38 @@ fun ContactsScreen(
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(searchVisible) {
|
||||
if (searchVisible) {
|
||||
// kotlinx.coroutines.delay(50)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
if (searchVisible) {
|
||||
ContactsSearchBar(
|
||||
query = searchQuery,
|
||||
focusRequester = focusRequester,
|
||||
clipboard = clipboard,
|
||||
scope = scope,
|
||||
onQueryChange = { searchQuery = it },
|
||||
onClear = { searchQuery = "" }
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
when (val state = uiState) {
|
||||
is ContactsUiState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
@@ -63,21 +111,32 @@ fun ContactsScreen(
|
||||
}
|
||||
|
||||
is ContactsUiState.Success -> {
|
||||
if (state.contacts.isEmpty()) {
|
||||
val filteredContacts = remember(state.contacts, searchQuery) {
|
||||
if (searchQuery.isBlank()) state.contacts
|
||||
else state.contacts.filter { item ->
|
||||
val contact = item.contact
|
||||
val name = contact.localAlias
|
||||
?: contact.displayName
|
||||
?: contact.name
|
||||
?: ""
|
||||
val addressMatch = item.addresses.any { addr ->
|
||||
addr.address.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
name.contains(searchQuery, ignoreCase = true) || addressMatch
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContacts.isEmpty()) {
|
||||
ContactsEmptyContent(
|
||||
onAdd = { showCreateSheet = true },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
items(state.contacts, key = { it.contact.id }) { item ->
|
||||
items(filteredContacts, key = { it.contact.id }) { item ->
|
||||
ContactRow(item, onClick = { onContactClick(item.contact.id) })
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
@@ -100,8 +159,9 @@ fun ContactsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ────────────────────────────────────────────────────────────────
|
||||
// ── Empty state ──────────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun ContactsEmptyContent(
|
||||
@@ -140,7 +200,65 @@ private fun ContactsEmptyContent(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Contact row ────────────────────────────────────────────────────────────────
|
||||
// ── Search bar ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun ContactsSearchBar(
|
||||
query: String,
|
||||
focusRequester: FocusRequester,
|
||||
clipboard: androidx.compose.ui.platform.Clipboard,
|
||||
scope: CoroutineScope,
|
||||
onQueryChange: (String) -> Unit,
|
||||
onClear: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
placeholder = { Text("Search contacts") },
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
val pasted = clipboard.getClipEntry()
|
||||
?.clipData?.getItemAt(0)?.text?.toString().orEmpty()
|
||||
onQueryChange(pasted)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentPaste,
|
||||
contentDescription = "Paste"
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onClear,
|
||||
enabled = query.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Clear search",
|
||||
tint = if (query.isNotEmpty()) LocalContentColor.current
|
||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Contact row ──────────────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun ContactRow(
|
||||
|
||||
Reference in New Issue
Block a user