From c9346412e6741db4058797c429f87c3ce6e59100 Mon Sep 17 00:00:00 2001 From: Danial Ramzan Date: Mon, 8 Dec 2025 07:12:05 -0800 Subject: [PATCH] Implemented search bar in split-tunneling page. Signed-off-by: Danial Ramzan Signed-off-by: danialramzan --- .../ipn/ui/view/SplitTunnelAppPickerView.kt | 109 +++++++++++++++++- android/src/main/res/values/strings.xml | 1 + 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt index 0317765..3417789 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt @@ -4,18 +4,29 @@ package com.tailscale.ipn.ui.view import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -51,6 +62,15 @@ fun SplitTunnelAppPickerView( val splitEnabled = remember { mutableStateOf(App.get().isSplitTunnelEnabled())} val currentSplitMode = remember { mutableStateOf(App.get().getSplitTunnelMode())} + val searchQuery = remember { mutableStateOf("") } + + val filteredApps = installedApps.filter { app -> + searchQuery.value.isBlank() || + app.name.contains(searchQuery.value, ignoreCase = true) || + app.packageName.contains(searchQuery.value, ignoreCase = true) + } + + Scaffold(topBar = { Header(titleRes = R.string.split_tunneling, onBack = backToSettings) }) { @@ -111,7 +131,16 @@ fun SplitTunnelAppPickerView( if (splitEnabled.value) { item("resolversHeader") { - Row(modifier = Modifier.padding(16.dp)) { + + Spacer(modifier = Modifier.height(8.dp)) + + + AppSearchBar( + query = searchQuery.value, + onQueryChange = { searchQuery.value = it } + ) + + Row(modifier = Modifier.padding(horizontal = 8.dp)) { FilterChip( selected = currentSplitMode.value == SplitTunnelMode.EXCLUDE, onClick = { @@ -123,6 +152,7 @@ fun SplitTunnelAppPickerView( Spacer(modifier = Modifier.width(8.dp)) + FilterChip( selected = currentSplitMode.value == SplitTunnelMode.INCLUDE, onClick = { @@ -145,7 +175,24 @@ fun SplitTunnelAppPickerView( ) ) } - items(installedApps) { app -> + + if (filteredApps.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(vertical = 24.dp, horizontal = 16.dp) + ) { + Text( + "No apps found", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + items(filteredApps) { app -> ListItem( headlineContent = { Text(app.name, fontWeight = FontWeight.SemiBold) }, leadingContent = { @@ -191,7 +238,25 @@ fun SplitTunnelAppPickerView( ) ) } - items(installedApps) { app -> + + if (filteredApps.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(vertical = 24.dp, horizontal = 16.dp) + ) { + Text( + "No apps found", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + + items(filteredApps) { app -> ListItem( headlineContent = { Text(app.name, fontWeight = FontWeight.SemiBold) }, leadingContent = { @@ -225,6 +290,8 @@ fun SplitTunnelAppPickerView( }) }) Lists.ItemDivider() + + } @@ -234,4 +301,38 @@ fun SplitTunnelAppPickerView( } } } -} \ No newline at end of file +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppSearchBar( + query: String, + onQueryChange: (String) -> Unit +) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp) + .background(MaterialTheme.colorScheme.surfaceContainer), + value = query, + onValueChange = onQueryChange, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search" + ) + }, + shape = SearchBarDefaults.dockedShape, + placeholder = { Text(stringResource(R.string.search_apps_ellipsis)) }, + singleLine = true, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { onQueryChange("") }) { + Icon(Icons.Default.Clear, contentDescription = "Clear search") + } + } + } + ) +} + + diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 9e3f2f0..3d00045 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Warning Search Search... + Search apps... Dismiss No results Back