diff --git a/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt b/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt index b45957d..03c6b0a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt @@ -11,6 +11,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.Typography @@ -205,3 +207,21 @@ val ColorScheme.secondaryButton: ButtonColors val ColorScheme.disabled: Color get() = Color(0xFFAFACAB) // gray-400 + +@OptIn(ExperimentalMaterial3Api::class) +val ColorScheme.searchBarColors: TextFieldColors + @Composable + get() { + val defaults = OutlinedTextFieldDefaults.colors() + return OutlinedTextFieldDefaults.colors( + focusedLeadingIconColor = MaterialTheme.colorScheme.onSurface, + unfocusedLeadingIconColor = MaterialTheme.colorScheme.onSurface, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedContainerColor = MaterialTheme.colorScheme.background, + unfocusedContainerColor = MaterialTheme.colorScheme.background, + disabledContainerColor = MaterialTheme.colorScheme.background, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent) + } diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/Styles.kt b/android/src/main/java/com/tailscale/ipn/ui/util/Styles.kt deleted file mode 100644 index 0df28ae..0000000 --- a/android/src/main/java/com/tailscale/ipn/ui/util/Styles.kt +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package com.tailscale.ipn.ui.util - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp - -@Composable -fun settingsRowModifier(): Modifier { - return Modifier.clip(shape = RoundedCornerShape(8.dp)).fillMaxWidth() -} diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 2e77210..6dff210 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -24,6 +24,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button @@ -33,16 +35,20 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -66,6 +72,7 @@ import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.theme.disabled import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.primaryListItem +import com.tailscale.ipn.ui.theme.searchBarColors import com.tailscale.ipn.ui.theme.secondaryButton import com.tailscale.ipn.ui.theme.surfaceContainerListItem import com.tailscale.ipn.ui.util.Lists @@ -318,75 +325,94 @@ fun PeerList( val searchTermStr by viewModel.searchTerm.collectAsState(initial = "") val netmap = viewModel.netmap.collectAsState() - SearchBar( - query = searchTermStr, - onQueryChange = onSearch, - onSearch = onSearch, - active = true, - onActiveChange = {}, - shape = RoundedCornerShape(10.dp), - leadingIcon = { Icon(Icons.Outlined.Search, null) }, - trailingIcon = { - if (searchTermStr.isNotEmpty()) ClearButton({ onSearch("") }) else CloseButton() - }, - tonalElevation = 0.dp, - shadowElevation = 0.dp, - colors = - SearchBarDefaults.colors( - containerColor = MaterialTheme.colorScheme.surface, - dividerColor = MaterialTheme.colorScheme.outline), - modifier = Modifier.fillMaxWidth()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - var first = true - peerList.value.forEach { peerSet -> - if (!first) { - item(key = "spacer_${peerSet.user?.DisplayName}") { - Lists.ItemDivider() - Spacer(Modifier.height(24.dp)) - } - } - first = false - - stickyHeader { - ListItem( - modifier = Modifier.heightIn(max = 48.dp), - headlineContent = { - Text( - text = - peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold) - }) - } + val focusManager = LocalFocusManager.current + var isFocussed by remember { mutableStateOf(false) } - itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> - ListItem( - modifier = Modifier.clickable { onNavigateToPeerDetails(peer) }, - colors = MaterialTheme.colorScheme.listItem, - headlineContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = - Modifier.padding(top = 2.dp) - .size(10.dp) - .background( - color = peer.connectedColor(netmap.value), - shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) - Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) - } - }, - supportingContent = { - Text( - text = peer.Addresses?.first()?.split("/")?.first() ?: "", - style = MaterialTheme.typography.bodyMedium) - }) - } + Box(modifier = Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surface)) { + OutlinedTextField( + modifier = + Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).onFocusChanged { + isFocussed = it.isFocused + }, + singleLine = true, + shape = MaterialTheme.shapes.large, + colors = MaterialTheme.colorScheme.searchBarColors, + leadingIcon = { Icon(imageVector = Icons.Outlined.Search, contentDescription = "search") }, + trailingIcon = { + if (isFocussed) { + IconButton( + onClick = { + focusManager.clearFocus() + onSearch("") + }) { + Icon( + imageVector = + if (searchTermStr.isEmpty()) Icons.Outlined.Close + else Icons.Outlined.Clear, + contentDescription = "clear search", + tint = MaterialTheme.colorScheme.onSurfaceVariant) + } } + }, + placeholder = { + Text( + text = stringResource(id = R.string.search), + style = MaterialTheme.typography.bodyLarge, + maxLines = 1) + }, + value = searchTermStr, + onValueChange = { onSearch(it) }) + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + var first = true + peerList.value.forEach { peerSet -> + if (!first) { + item(key = "spacer_${peerSet.user?.DisplayName}") { + Lists.ItemDivider() + Spacer(Modifier.height(24.dp)) } } + first = false + + stickyHeader { + ListItem( + modifier = Modifier.heightIn(max = 48.dp), + headlineContent = { + Text( + text = peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold) + }) + } + + itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> + ListItem( + modifier = Modifier.clickable { onNavigateToPeerDetails(peer) }, + colors = MaterialTheme.colorScheme.listItem, + headlineContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.padding(top = 2.dp) + .size(10.dp) + .background( + color = peer.connectedColor(netmap.value), + shape = RoundedCornerShape(percent = 50))) {} + Spacer(modifier = Modifier.size(8.dp)) + Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) + } + }, + supportingContent = { + Text( + text = peer.Addresses?.first()?.split("/")?.first() ?: "", + style = MaterialTheme.typography.bodyMedium) + }) + } + } + } } @OptIn(ExperimentalPermissionsApi::class) diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index ddf00d1..0c7c51a 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ Offline OK Warning + Search\n Tailscale