Skip to content

Commit 10b2c61

Browse files
authored
android: refine search (#611)
-improve transition -clean up search input spacing to match other elements -match search results page styling to machines page -fix issue where search suggestions were propagating to main view -flip new search flag On Fixes tailscale/corp#18973 Signed-off-by: kari-ts <[email protected]>
1 parent 6a3342e commit 10b2c61

File tree

12 files changed

+385
-223
lines changed

12 files changed

+385
-223
lines changed

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ dependencies {
119119
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
120120
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1"
121121
implementation 'junit:junit:4.13.2'
122+
implementation 'androidx.room:room-ktx:2.6.1'
122123
runtimeOnly "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
123124
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
124125
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

android/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
android:label="Tailscale"
3838
android:requestLegacyExternalStorage="true"
3939
android:roundIcon="@mipmap/ic_launcher_round"
40+
android:enableOnBackInvokedCallback="true"
4041
android:theme="@style/Theme.App.SplashScreen">
4142
<activity
4243
android:name="MainActivity"

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import com.tailscale.ipn.ui.notifier.HealthNotifier
3434
import com.tailscale.ipn.ui.notifier.Notifier
3535
import com.tailscale.ipn.ui.viewModel.VpnViewModel
3636
import com.tailscale.ipn.ui.viewModel.VpnViewModelFactory
37-
import com.tailscale.ipn.util.TSLog
3837
import com.tailscale.ipn.util.FeatureFlags
38+
import com.tailscale.ipn.util.TSLog
3939
import kotlinx.coroutines.CoroutineScope
4040
import kotlinx.coroutines.Dispatchers
4141
import kotlinx.coroutines.SupervisorJob
@@ -192,7 +192,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
192192
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
193193
}
194194
TSLog.init(this)
195-
FeatureFlags.initialize(mapOf("enable_new_search" to false))
195+
FeatureFlags.initialize(mapOf("enable_new_search" to true))
196196
}
197197

198198
private fun initViewModels() {

android/src/main/java/com/tailscale/ipn/MainActivity.kt

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
1414
import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
1515
import android.net.ConnectivityManager
1616
import android.net.NetworkCapabilities
17+
import android.os.Build
1718
import android.os.Bundle
1819
import android.provider.Settings
20+
import android.util.Log
1921
import androidx.activity.ComponentActivity
2022
import androidx.activity.compose.setContent
2123
import androidx.activity.result.ActivityResultLauncher
2224
import androidx.activity.result.contract.ActivityResultContract
25+
import androidx.annotation.RequiresApi
2326
import androidx.browser.customtabs.CustomTabsIntent
27+
import androidx.compose.animation.core.LinearOutSlowInEasing
2428
import androidx.compose.animation.core.tween
2529
import androidx.compose.animation.fadeIn
2630
import androidx.compose.animation.fadeOut
@@ -111,6 +115,7 @@ class MainActivity : ComponentActivity() {
111115
// simply opening the URL. This should be consumed once it has been handled.
112116
private val loginQRCode: StateFlow<String?> = MutableStateFlow(null)
113117

118+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
114119
@SuppressLint("SourceLockedOrientationActivity")
115120
override fun onCreate(savedInstanceState: Bundle?) {
116121
super.onCreate(savedInstanceState)
@@ -146,24 +151,37 @@ class MainActivity : ComponentActivity() {
146151
viewModel.setVpnPermissionLauncher(vpnPermissionLauncher)
147152

148153
setContent {
154+
navController = rememberNavController()
155+
149156
AppTheme {
150-
navController = rememberNavController()
151157
Surface(color = MaterialTheme.colorScheme.inverseSurface) { // Background for the letterbox
152158
Surface(modifier = Modifier.universalFit()) { // Letterbox for AndroidTV
153159
NavHost(
154160
navController = navController,
155161
startDestination = "main",
156162
enterTransition = {
157-
slideInHorizontally(animationSpec = tween(150), initialOffsetX = { it })
163+
slideInHorizontally(
164+
animationSpec = tween(250, easing = LinearOutSlowInEasing),
165+
initialOffsetX = { it }) +
166+
fadeIn(animationSpec = tween(500, easing = LinearOutSlowInEasing))
158167
},
159168
exitTransition = {
160-
slideOutHorizontally(animationSpec = tween(150), targetOffsetX = { -it })
169+
slideOutHorizontally(
170+
animationSpec = tween(250, easing = LinearOutSlowInEasing),
171+
targetOffsetX = { -it }) +
172+
fadeOut(animationSpec = tween(500, easing = LinearOutSlowInEasing))
161173
},
162174
popEnterTransition = {
163-
slideInHorizontally(animationSpec = tween(150), initialOffsetX = { -it })
175+
slideInHorizontally(
176+
animationSpec = tween(250, easing = LinearOutSlowInEasing),
177+
initialOffsetX = { -it }) +
178+
fadeIn(animationSpec = tween(500, easing = LinearOutSlowInEasing))
164179
},
165180
popExitTransition = {
166-
slideOutHorizontally(animationSpec = tween(150), targetOffsetX = { it })
181+
slideOutHorizontally(
182+
animationSpec = tween(250, easing = LinearOutSlowInEasing),
183+
targetOffsetX = { it }) +
184+
fadeOut(animationSpec = tween(500, easing = LinearOutSlowInEasing))
167185
}) {
168186
fun backTo(route: String): () -> Unit = {
169187
navController.popBackStack(route = route, inclusive = false)
@@ -177,7 +195,10 @@ class MainActivity : ComponentActivity() {
177195
},
178196
onNavigateToExitNodes = { navController.navigate("exitNodes") },
179197
onNavigateToHealth = { navController.navigate("health") },
180-
onNavigateToSearch = { navController.navigate("search") })
198+
onNavigateToSearch = {
199+
viewModel.enableSearchAutoFocus()
200+
navController.navigate("search")
201+
})
181202

182203
val settingsNav =
183204
SettingsNav(
@@ -186,7 +207,7 @@ class MainActivity : ComponentActivity() {
186207
onNavigateToDNSSettings = { navController.navigate("dnsSettings") },
187208
onNavigateToSplitTunneling = { navController.navigate("splitTunneling") },
188209
onNavigateToTailnetLock = { navController.navigate("tailnetLock") },
189-
onNavigateToSubnetRouting = { navController.navigate("subnetRouting")},
210+
onNavigateToSubnetRouting = { navController.navigate("subnetRouting") },
190211
onNavigateToMDMSettings = { navController.navigate("mdmSettings") },
191212
onNavigateToManagedBy = { navController.navigate("managedBy") },
192213
onNavigateToUserSwitcher = { navController.navigate("userSwitcher") },
@@ -219,11 +240,14 @@ class MainActivity : ComponentActivity() {
219240
MainView(loginAtUrl = ::login, navigation = mainViewNav, viewModel = viewModel)
220241
}
221242
composable("search") {
243+
val autoFocus = viewModel.autoFocusSearch
222244
SearchView(
223245
viewModel = viewModel,
224246
navController = navController,
225-
onNavigateBack = { navController.popBackStack() })
226-
}
247+
onNavigateBack = { navController.popBackStack() },
248+
autoFocus = autoFocus
249+
)
250+
}
227251
composable("settings") { SettingsView(settingsNav) }
228252
composable("exitNodes") { ExitNodePicker(exitNodePickerNav) }
229253
composable("health") { HealthView(backTo("main")) }

android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme
1818
import androidx.compose.material3.Text
1919
import androidx.compose.runtime.Composable
2020
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.Color
2122
import androidx.compose.ui.graphics.RectangleShape
2223
import androidx.compose.ui.text.AnnotatedString
2324
import androidx.compose.ui.text.TextStyle
@@ -34,7 +35,8 @@ object Lists {
3435

3536
@Composable
3637
fun ItemDivider() {
37-
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
38+
HorizontalDivider(
39+
color = MaterialTheme.colorScheme.outlineVariant, modifier = Modifier.fillMaxWidth())
3840
}
3941

4042
@Composable
@@ -43,20 +45,36 @@ object Lists {
4345
bottomPadding: Dp = 0.dp,
4446
style: TextStyle = MaterialTheme.typography.titleMedium,
4547
fontWeight: FontWeight? = null,
46-
focusable: Boolean = false
48+
focusable: Boolean = false,
49+
backgroundColor: Color = MaterialTheme.colorScheme.surface,
50+
fontColor: Color? = null
4751
) {
48-
Box(
49-
modifier =
50-
Modifier.fillMaxWidth()
51-
.background(color = MaterialTheme.colorScheme.surface, shape = RectangleShape)) {
52-
Text(
53-
title,
54-
modifier =
55-
Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = bottomPadding)
52+
Box(
53+
modifier = Modifier
54+
.fillMaxWidth()
55+
.background(color = backgroundColor, shape = RectangleShape)
56+
) {
57+
if (fontColor != null) {
58+
Text(
59+
text = title,
60+
modifier = Modifier
61+
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = bottomPadding)
5662
.focusable(focusable),
57-
style = style,
58-
fontWeight = fontWeight)
59-
}
63+
style = style,
64+
fontWeight = fontWeight,
65+
color = fontColor
66+
)
67+
} else {
68+
Text(
69+
text = title,
70+
modifier = Modifier
71+
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = bottomPadding)
72+
.focusable(focusable),
73+
style = style,
74+
fontWeight = fontWeight
75+
)
76+
}
77+
}
6078
}
6179

6280
@Composable

android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fun Avatar(
5151
modifier =
5252
Modifier.conditional(AndroidTVUtil.isAndroidTV(), { padding(4.dp) })
5353
.conditional(
54-
AndroidTVUtil.isAndroidTV() && isFocusable,
54+
AndroidTVUtil.isAndroidTV() && isFocusable,
5555
{
5656
size((size * 1.5f).dp) // Focusable area is larger than the avatar
5757
})

android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ enum class ErrorDialogType {
5353

5454
@Composable
5555
fun ErrorDialog(type: ErrorDialogType, action: () -> Unit = {}) {
56-
ErrorDialog(
56+
ErrorDialog(
5757
title = type.title,
5858
message = stringResource(id = type.message),
5959
buttonText = type.buttonText,
@@ -68,7 +68,7 @@ fun ErrorDialog(
6868
@StringRes buttonText: Int = R.string.ok,
6969
onDismiss: () -> Unit = {}
7070
) {
71-
ErrorDialog(
71+
ErrorDialog(
7272
title = title,
7373
message = stringResource(id = message),
7474
buttonText = buttonText,

android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.width
2525
import androidx.compose.foundation.lazy.LazyColumn
2626
import androidx.compose.foundation.shape.RoundedCornerShape
2727
import androidx.compose.material.icons.Icons
28-
import androidx.compose.material.icons.filled.Search
2928
import androidx.compose.material.icons.outlined.ArrowDropDown
3029
import androidx.compose.material.icons.outlined.Clear
3130
import androidx.compose.material.icons.outlined.Close
@@ -46,6 +45,7 @@ import androidx.compose.material3.OutlinedTextField
4645
import androidx.compose.material3.Scaffold
4746
import androidx.compose.material3.Text
4847
import androidx.compose.runtime.Composable
48+
import androidx.compose.runtime.DisposableEffect
4949
import androidx.compose.runtime.LaunchedEffect
5050
import androidx.compose.runtime.collectAsState
5151
import androidx.compose.runtime.derivedStateOf
@@ -545,8 +545,6 @@ fun PeerList(
545545
Column(modifier = Modifier.fillMaxSize()) {
546546
if (enableSearch && FeatureFlags.isEnabled("enable_new_search")) {
547547
Search(onSearchBarClick)
548-
549-
Spacer(modifier = Modifier.height(if (showNoResults) 0.dp else 8.dp))
550548
} else {
551549
if (enableSearch) {
552550
Box(
@@ -748,37 +746,54 @@ fun PromptPermissionsIfNecessary() {
748746
@OptIn(ExperimentalMaterial3Api::class)
749747
@Composable
750748
fun Search(
751-
onSearchBarClick: () -> Unit // Callback for navigating to SearchView
749+
onSearchBarClick: () -> Unit, // Callback for navigating to SearchView
750+
backgroundColor: Color = MaterialTheme.colorScheme.background // Default background color
752751
) {
753752
// Prevent multiple taps
754753
var isNavigating by remember { mutableStateOf(false) }
755754

756-
// Outer Box to handle clicks
757755
Box(
758756
modifier =
759757
Modifier.fillMaxWidth()
760-
.height(56.dp)
761-
.clip(RoundedCornerShape(28.dp))
762758
.background(MaterialTheme.colorScheme.surface)
763-
.clickable(enabled = !isNavigating) { // Intercept taps
764-
isNavigating = true
765-
onSearchBarClick() // Trigger navigation
766-
}
767-
.padding(horizontal = 16.dp)) {
768-
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) {
769-
Icon(
770-
imageVector = Icons.Default.Search,
771-
contentDescription = stringResource(R.string.search),
772-
tint = MaterialTheme.colorScheme.onSurfaceVariant,
773-
modifier = Modifier.padding(start = 16.dp))
774-
Spacer(modifier = Modifier.width(8.dp))
775-
// Placeholder Text
776-
Text(
777-
text = stringResource(R.string.search_ellipsis),
778-
style = MaterialTheme.typography.bodyMedium,
779-
color = MaterialTheme.colorScheme.onSurfaceVariant,
780-
modifier = Modifier.weight(1f))
781-
}
759+
.padding(top = 8.dp)) {
760+
Box(
761+
modifier =
762+
Modifier.fillMaxWidth()
763+
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
764+
.height(56.dp)
765+
.clip(MaterialTheme.shapes.extraLarge) // Rounded corners for search bar
766+
.background(backgroundColor) // Search bar background
767+
.clickable(enabled = !isNavigating) { // Intercept taps
768+
isNavigating = true
769+
onSearchBarClick()
770+
}
771+
.padding(horizontal = 16.dp) // Internal padding
772+
) {
773+
Row(
774+
verticalAlignment = Alignment.CenterVertically, // Ensure icon aligns with text
775+
modifier = Modifier.fillMaxSize()) {
776+
// Leading Icon
777+
Icon(
778+
imageVector = Icons.Outlined.Search,
779+
contentDescription = "Search",
780+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
781+
modifier =
782+
Modifier.padding(start = 0.dp) // Optional start padding for alignment
783+
)
784+
Spacer(modifier = Modifier.width(4.dp))
785+
786+
// Placeholder Text
787+
Text(
788+
text = stringResource(R.string.search_ellipsis),
789+
style = MaterialTheme.typography.bodyLarge,
790+
color = MaterialTheme.colorScheme.onSurfaceVariant,
791+
maxLines = 1,
792+
overflow = TextOverflow.Ellipsis,
793+
modifier = Modifier.weight(1f) // Ensure text takes up remaining space
794+
)
795+
}
796+
}
782797
}
783798
}
784799

0 commit comments

Comments
 (0)