44package com.tailscale.ipn.ui.view
55
66import androidx.compose.foundation.Image
7+ import androidx.compose.foundation.background
8+ import androidx.compose.foundation.layout.Box
79import androidx.compose.foundation.layout.Row
810import androidx.compose.foundation.layout.Spacer
11+ import androidx.compose.foundation.layout.fillMaxWidth
912import androidx.compose.foundation.layout.height
1013import androidx.compose.foundation.layout.padding
1114import androidx.compose.foundation.layout.width
1215import androidx.compose.foundation.lazy.LazyColumn
1316import 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
1420import androidx.compose.material3.Checkbox
21+ import androidx.compose.material3.ExperimentalMaterial3Api
1522import androidx.compose.material3.FilterChip
23+ import androidx.compose.material3.Icon
24+ import androidx.compose.material3.IconButton
1625import androidx.compose.material3.ListItem
1726import androidx.compose.material3.MaterialTheme
27+ import androidx.compose.material3.OutlinedTextField
1828import androidx.compose.material3.Scaffold
29+ import androidx.compose.material3.SearchBarDefaults
1930import androidx.compose.material3.Text
2031import androidx.compose.runtime.Composable
2132import 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+
0 commit comments