Skip to content

Commit 0d691f4

Browse files
committed
Implemented search bar in split-tunneling page.
Signed-off-by: Danial Ramzan <[email protected]>
1 parent dff9793 commit 0d691f4

File tree

2 files changed

+106
-4
lines changed

2 files changed

+106
-4
lines changed

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

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,29 @@
44
package com.tailscale.ipn.ui.view
55

66
import androidx.compose.foundation.Image
7+
import androidx.compose.foundation.background
8+
import androidx.compose.foundation.layout.Box
79
import androidx.compose.foundation.layout.Row
810
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.fillMaxWidth
912
import androidx.compose.foundation.layout.height
1013
import androidx.compose.foundation.layout.padding
1114
import androidx.compose.foundation.layout.width
1215
import androidx.compose.foundation.lazy.LazyColumn
1316
import androidx.compose.foundation.lazy.items
17+
import androidx.compose.material.icons.Icons
18+
import androidx.compose.material.icons.filled.Clear
19+
import androidx.compose.material.icons.filled.Search
1420
import androidx.compose.material3.Checkbox
21+
import androidx.compose.material3.ExperimentalMaterial3Api
1522
import androidx.compose.material3.FilterChip
23+
import androidx.compose.material3.Icon
24+
import androidx.compose.material3.IconButton
1625
import androidx.compose.material3.ListItem
1726
import androidx.compose.material3.MaterialTheme
27+
import androidx.compose.material3.OutlinedTextField
1828
import androidx.compose.material3.Scaffold
29+
import androidx.compose.material3.SearchBarDefaults
1930
import androidx.compose.material3.Text
2031
import androidx.compose.runtime.Composable
2132
import androidx.compose.runtime.collectAsState
@@ -51,6 +62,15 @@ fun SplitTunnelAppPickerView(
5162

5263
val splitEnabled = remember { mutableStateOf(App.get().isSplitTunnelEnabled())}
5364
val currentSplitMode = remember { mutableStateOf(App.get().getSplitTunnelMode())}
65+
val searchQuery = remember { mutableStateOf("") }
66+
67+
val filteredApps = installedApps.filter { app ->
68+
searchQuery.value.isBlank() ||
69+
app.name.contains(searchQuery.value, ignoreCase = true) ||
70+
app.packageName.contains(searchQuery.value, ignoreCase = true)
71+
}
72+
73+
5474

5575

5676
Scaffold(topBar = { Header(titleRes = R.string.split_tunneling, onBack = backToSettings) }) {
@@ -111,7 +131,16 @@ fun SplitTunnelAppPickerView(
111131
if (splitEnabled.value) {
112132

113133
item("resolversHeader") {
114-
Row(modifier = Modifier.padding(16.dp)) {
134+
135+
Spacer(modifier = Modifier.height(8.dp))
136+
137+
138+
AppSearchBar(
139+
query = searchQuery.value,
140+
onQueryChange = { searchQuery.value = it }
141+
)
142+
143+
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
115144
FilterChip(
116145
selected = currentSplitMode.value == SplitTunnelMode.EXCLUDE,
117146
onClick = {
@@ -123,6 +152,7 @@ fun SplitTunnelAppPickerView(
123152

124153
Spacer(modifier = Modifier.width(8.dp))
125154

155+
126156
FilterChip(
127157
selected = currentSplitMode.value == SplitTunnelMode.INCLUDE,
128158
onClick = {
@@ -145,7 +175,24 @@ fun SplitTunnelAppPickerView(
145175
)
146176
)
147177
}
148-
items(installedApps) { app ->
178+
179+
if (filteredApps.isEmpty()) {
180+
item {
181+
Box(
182+
modifier = Modifier
183+
.fillMaxWidth()
184+
.background(MaterialTheme.colorScheme.surface)
185+
.padding(vertical = 24.dp, horizontal = 16.dp)
186+
) {
187+
Text(
188+
"No apps found",
189+
color = MaterialTheme.colorScheme.onSurfaceVariant
190+
)
191+
}
192+
}
193+
}
194+
195+
items(filteredApps) { app ->
149196
ListItem(
150197
headlineContent = { Text(app.name, fontWeight = FontWeight.SemiBold) },
151198
leadingContent = {
@@ -191,7 +238,25 @@ fun SplitTunnelAppPickerView(
191238
)
192239
)
193240
}
194-
items(installedApps) { app ->
241+
242+
if (filteredApps.isEmpty()) {
243+
item {
244+
Box(
245+
modifier = Modifier
246+
.fillMaxWidth()
247+
.background(MaterialTheme.colorScheme.surface)
248+
.padding(vertical = 24.dp, horizontal = 16.dp)
249+
) {
250+
Text(
251+
"No apps found",
252+
color = MaterialTheme.colorScheme.onSurfaceVariant
253+
)
254+
}
255+
}
256+
}
257+
258+
259+
items(filteredApps) { app ->
195260
ListItem(
196261
headlineContent = { Text(app.name, fontWeight = FontWeight.SemiBold) },
197262
leadingContent = {
@@ -225,6 +290,8 @@ fun SplitTunnelAppPickerView(
225290
})
226291
})
227292
Lists.ItemDivider()
293+
294+
228295
}
229296

230297

@@ -234,4 +301,38 @@ fun SplitTunnelAppPickerView(
234301
}
235302
}
236303
}
237-
}
304+
}
305+
306+
@OptIn(ExperimentalMaterial3Api::class)
307+
@Composable
308+
fun AppSearchBar(
309+
query: String,
310+
onQueryChange: (String) -> Unit
311+
) {
312+
OutlinedTextField(
313+
modifier = Modifier
314+
.fillMaxWidth()
315+
.padding(horizontal = 8.dp, vertical = 8.dp)
316+
.background(MaterialTheme.colorScheme.surfaceContainer),
317+
value = query,
318+
onValueChange = onQueryChange,
319+
leadingIcon = {
320+
Icon(
321+
imageVector = Icons.Default.Search,
322+
contentDescription = "Search"
323+
)
324+
},
325+
shape = SearchBarDefaults.dockedShape,
326+
placeholder = { Text(stringResource(R.string.search_apps_ellipsis)) },
327+
singleLine = true,
328+
trailingIcon = {
329+
if (query.isNotEmpty()) {
330+
IconButton(onClick = { onQueryChange("") }) {
331+
Icon(Icons.Default.Clear, contentDescription = "Clear search")
332+
}
333+
}
334+
}
335+
)
336+
}
337+
338+

android/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<string name="warning">Warning</string>
2121
<string name="search">Search</string>
2222
<string name="search_ellipsis">Search...</string>
23+
<string name="search_apps_ellipsis">Search apps...</string>
2324
<string name="dismiss">Dismiss</string>
2425
<string name="no_results">No results</string>
2526
<string name="back">Back</string>

0 commit comments

Comments
 (0)