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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
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.Person
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -29,6 +38,8 @@ fun ContactsScreen(
|
|||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
var showCreateSheet by remember { mutableStateOf(false) }
|
var showCreateSheet by remember { mutableStateOf(false) }
|
||||||
|
var searchVisible by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -41,6 +52,17 @@ fun ContactsScreen(
|
|||||||
contentDescription = strings.back
|
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 ->
|
) { 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) {
|
when (val state = uiState) {
|
||||||
is ContactsUiState.Loading -> {
|
is ContactsUiState.Loading -> {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -63,21 +111,32 @@ fun ContactsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ContactsUiState.Success -> {
|
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(
|
ContactsEmptyContent(
|
||||||
onAdd = { showCreateSheet = true },
|
onAdd = { showCreateSheet = true },
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding),
|
|
||||||
contentPadding = PaddingValues(vertical = 8.dp)
|
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) })
|
ContactRow(item, onClick = { onContactClick(item.contact.id) })
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
@@ -99,9 +158,10 @@ fun ContactsScreen(
|
|||||||
onDismiss = { showCreateSheet = false }
|
onDismiss = { showCreateSheet = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Empty state ────────────────────────────────────────────────────────────────
|
// ── Empty state ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContactsEmptyContent(
|
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
|
@Composable
|
||||||
private fun ContactRow(
|
private fun ContactRow(
|
||||||
|
|||||||
Reference in New Issue
Block a user