diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactsScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactsScreen.kt index cacae0e..6bb004e 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactsScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactsScreen.kt @@ -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,58 +72,96 @@ fun ContactsScreen( } } ) { innerPadding -> - when (val state = uiState) { - is ContactsUiState.Loading -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + 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() } - is ContactsUiState.Success -> { - if (state.contacts.isEmpty()) { - ContactsEmptyContent( - onAdd = { showCreateSheet = true }, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - contentPadding = PaddingValues(vertical = 8.dp) + when (val state = uiState) { + is ContactsUiState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { - items(state.contacts, key = { it.contact.id }) { item -> - ContactRow(item, onClick = { onContactClick(item.contact.id) }) - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - thickness = 0.5.dp - ) + CircularProgressIndicator() + } + } + + is ContactsUiState.Success -> { + 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() + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + items(filteredContacts, key = { it.contact.id }) { item -> + ContactRow(item, onClick = { onContactClick(item.contact.id) }) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 0.5.dp + ) + } } } } } } - } - if (showCreateSheet) { - CreateContactSheet( - onSave = { name, addressType, address -> - viewModel.createContact(name, addressType, address) - showCreateSheet = false - }, - onDismiss = { showCreateSheet = false } - ) + if (showCreateSheet) { + CreateContactSheet( + onSave = { name, addressType, address -> + viewModel.createContact(name, addressType, address) + showCreateSheet = false + }, + onDismiss = { showCreateSheet = false } + ) + } } } -// ── 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(