44package com.tailscale.ipn.ui.view
55
66import androidx.compose.foundation.Image
7+ import androidx.compose.foundation.background
8+ import androidx.compose.foundation.layout.Row
79import androidx.compose.foundation.layout.height
810import androidx.compose.foundation.layout.padding
911import androidx.compose.foundation.layout.width
1012import androidx.compose.foundation.lazy.LazyColumn
1113import androidx.compose.foundation.lazy.items
14+ import androidx.compose.material.icons.Icons
15+ import androidx.compose.material.icons.filled.MoreVert
16+ import androidx.compose.material3.AlertDialog
1217import androidx.compose.material3.Checkbox
18+ import androidx.compose.material3.DropdownMenu
19+ import androidx.compose.material3.Icon
20+ import androidx.compose.material3.IconButton
1321import androidx.compose.material3.ListItem
1422import androidx.compose.material3.MaterialTheme
1523import androidx.compose.material3.Scaffold
@@ -27,6 +35,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
2735import com.tailscale.ipn.App
2836import com.tailscale.ipn.R
2937import com.tailscale.ipn.ui.util.Lists
38+ import com.tailscale.ipn.ui.util.set
3039import com.tailscale.ipn.ui.viewModel.SplitTunnelAppPickerViewModel
3140
3241@Composable
@@ -35,23 +44,39 @@ fun SplitTunnelAppPickerView(
3544 model : SplitTunnelAppPickerViewModel = viewModel()
3645) {
3746 val installedApps by model.installedApps.collectAsState()
38- val excludedPackageNames by model.excludedPackageNames.collectAsState()
47+ val selectedPackageNames by model.selectedPackageNames.collectAsState()
48+ val allowSelected by model.allowSelected.collectAsState()
3949 val builtInDisallowedPackageNames: List <String > = App .get().builtInDisallowedPackageNames
4050 val mdmIncludedPackages by model.mdmIncludedPackages.collectAsState()
4151 val mdmExcludedPackages by model.mdmExcludedPackages.collectAsState()
52+ val showHeaderMenu by model.showHeaderMenu.collectAsState()
53+ val showSwitchDialog by model.showSwitchDialog.collectAsState()
4254
43- Scaffold (topBar = { Header (titleRes = R .string.split_tunneling, onBack = backToSettings) }) {
44- innerPadding ->
45- LazyColumn (modifier = Modifier .padding(innerPadding)) {
46- item(key = " header" ) {
47- ListItem (
48- headlineContent = {
49- Text (
50- stringResource(
51- R .string
52- .selected_apps_will_access_the_internet_directly_without_using_tailscale))
55+ if (showSwitchDialog) {
56+ SwitchAlertDialog (
57+ onConfirm = {
58+ model.showSwitchDialog.set(false )
59+ model.performSelectionSwitch()
60+ },
61+ onDismiss = { model.showSwitchDialog.set(false ) })
62+ }
63+
64+ Scaffold (
65+ topBar = {
66+ Header (
67+ titleRes = R .string.split_tunneling,
68+ onBack = backToSettings,
69+ actions = {
70+ Row {
71+ FusMenu (viewModel = model, onSwitchClick = { model.showSwitchDialog.set(true ) })
72+ IconButton (onClick = { model.showHeaderMenu.set(! showHeaderMenu) }) {
73+ Icon (Icons .Default .MoreVert , " menu" )
74+ }
75+ }
5376 })
54- }
77+ },
78+ ) { innerPadding ->
79+ LazyColumn (modifier = Modifier .padding(innerPadding)) {
5580 if (mdmExcludedPackages.value?.isNotEmpty() == true ) {
5681 item(" mdmExcludedNotice" ) {
5782 ListItem (
@@ -67,9 +92,22 @@ fun SplitTunnelAppPickerView(
6792 })
6893 }
6994 } else {
95+ item(" header" ) {
96+ ListItem (
97+ headlineContent = {
98+ Text (
99+ stringResource(
100+ if (allowSelected) R .string.selected_apps_will_access_tailscale
101+ else
102+ R .string
103+ .selected_apps_will_access_the_internet_directly_without_using_tailscale))
104+ })
105+ }
70106 item(" resolversHeader" ) {
71107 Lists .SectionDivider (
72- stringResource(R .string.count_excluded_apps, excludedPackageNames.count()))
108+ stringResource(
109+ if (allowSelected) R .string.count_included_apps else R .string.count_excluded_apps,
110+ selectedPackageNames.count()))
73111 }
74112 items(installedApps) { app ->
75113 ListItem (
@@ -93,13 +131,13 @@ fun SplitTunnelAppPickerView(
93131 },
94132 trailingContent = {
95133 Checkbox (
96- checked = excludedPackageNames .contains(app.packageName),
134+ checked = selectedPackageNames .contains(app.packageName),
97135 enabled = ! builtInDisallowedPackageNames.contains(app.packageName),
98136 onCheckedChange = { checked ->
99137 if (checked) {
100- model.exclude (packageName = app.packageName)
138+ model.select (packageName = app.packageName)
101139 } else {
102- model.unexclude (packageName = app.packageName)
140+ model.deselect (packageName = app.packageName)
103141 }
104142 })
105143 })
@@ -109,3 +147,40 @@ fun SplitTunnelAppPickerView(
109147 }
110148 }
111149}
150+
151+ @Composable
152+ fun FusMenu (viewModel : SplitTunnelAppPickerViewModel , onSwitchClick : (() -> Unit )) {
153+ val expanded by viewModel.showHeaderMenu.collectAsState()
154+ val allowSelected by viewModel.allowSelected.collectAsState()
155+
156+ DropdownMenu (
157+ expanded = expanded,
158+ onDismissRequest = { viewModel.showHeaderMenu.set(false ) },
159+ modifier = Modifier .background(MaterialTheme .colorScheme.surfaceContainer)) {
160+ MenuItem (
161+ onClick = {
162+ viewModel.showHeaderMenu.set(false )
163+ onSwitchClick()
164+ },
165+ text =
166+ stringResource(
167+ if (allowSelected) R .string.switch_to_select_to_exclude
168+ else R .string.switch_to_select_to_include))
169+ }
170+ }
171+
172+ @Composable
173+ fun SwitchAlertDialog (onConfirm : (() -> Unit ), onDismiss : (() -> Unit )) {
174+ AlertDialog (
175+ title = { Text (text = stringResource(R .string.switch_warning_dialog_title)) },
176+ text = { Text (text = stringResource(R .string.switch_warning_dialog_description)) },
177+ onDismissRequest = onDismiss,
178+ confirmButton = {
179+ WarningActionButton (onClick = onConfirm) {
180+ Text (text = stringResource(R .string.confirm_switch))
181+ }
182+ },
183+ dismissButton = {
184+ DismissActionButton (onClick = onDismiss) { Text (text = stringResource(R .string.cancel)) }
185+ })
186+ }
0 commit comments