@@ -32,6 +32,7 @@ import androidx.compose.material.icons.outlined.Close
32
32
import androidx.compose.material.icons.outlined.Lock
33
33
import androidx.compose.material.icons.outlined.Search
34
34
import androidx.compose.material.icons.outlined.Settings
35
+ import androidx.compose.material3.AlertDialog
35
36
import androidx.compose.material3.Button
36
37
import androidx.compose.material3.DropdownMenu
37
38
import androidx.compose.material3.DropdownMenuItem
@@ -61,12 +62,14 @@ import androidx.compose.ui.focus.onFocusChanged
61
62
import androidx.compose.ui.graphics.Color
62
63
import androidx.compose.ui.platform.LocalClipboardManager
63
64
import androidx.compose.ui.platform.LocalFocusManager
65
+ import androidx.compose.ui.platform.LocalUriHandler
64
66
import androidx.compose.ui.res.painterResource
65
67
import androidx.compose.ui.res.stringResource
66
68
import androidx.compose.ui.text.SpanStyle
67
69
import androidx.compose.ui.text.buildAnnotatedString
68
70
import androidx.compose.ui.text.font.FontWeight
69
71
import androidx.compose.ui.text.style.TextAlign
72
+ import androidx.compose.ui.text.style.TextDecoration
70
73
import androidx.compose.ui.text.style.TextOverflow
71
74
import androidx.compose.ui.tooling.preview.Preview
72
75
import androidx.compose.ui.unit.dp
@@ -78,11 +81,13 @@ import com.tailscale.ipn.App
78
81
import com.tailscale.ipn.R
79
82
import com.tailscale.ipn.mdm.MDMSettings
80
83
import com.tailscale.ipn.mdm.ShowHide
84
+ import com.tailscale.ipn.ui.Links
81
85
import com.tailscale.ipn.ui.model.Ipn
82
86
import com.tailscale.ipn.ui.model.IpnLocal
83
87
import com.tailscale.ipn.ui.model.Netmap
84
88
import com.tailscale.ipn.ui.model.Permissions
85
89
import com.tailscale.ipn.ui.model.Tailcfg
90
+ import com.tailscale.ipn.ui.theme.AppTheme
86
91
import com.tailscale.ipn.ui.theme.customErrorContainer
87
92
import com.tailscale.ipn.ui.theme.disabled
88
93
import com.tailscale.ipn.ui.theme.errorButton
@@ -97,7 +102,6 @@ import com.tailscale.ipn.ui.theme.short
97
102
import com.tailscale.ipn.ui.theme.surfaceContainerListItem
98
103
import com.tailscale.ipn.ui.theme.warningButton
99
104
import com.tailscale.ipn.ui.theme.warningListItem
100
- import com.tailscale.ipn.ui.util.AndroidTVUtil
101
105
import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
102
106
import com.tailscale.ipn.ui.util.AutoResizingText
103
107
import com.tailscale.ipn.ui.util.Lists
@@ -124,7 +128,7 @@ data class MainViewNavigation(
124
128
fun MainView (
125
129
loginAtUrl : (String ) -> Unit ,
126
130
navigation : MainViewNavigation ,
127
- viewModel : MainViewModel
131
+ viewModel : MainViewModel ,
128
132
) {
129
133
val currentPingDevice by viewModel.pingViewModel.peer.collectAsState()
130
134
val healthIcon by viewModel.healthIcon.collectAsState()
@@ -147,6 +151,8 @@ fun MainView(
147
151
val showExitNodePicker by MDMSettings .exitNodesPicker.flow.collectAsState()
148
152
val disableToggle by MDMSettings .forceEnabled.flow.collectAsState()
149
153
val showKeyExpiry by viewModel.showExpiry.collectAsState(initial = false )
154
+ val showDirectoryPickerInterstitial by
155
+ viewModel.showDirectoryPickerInterstitial.collectAsState()
150
156
151
157
// Hide the header only on Android TV when the user needs to login
152
158
val hideHeader = (isAndroidTV() && state == Ipn .State .NeedsLogin )
@@ -214,8 +220,8 @@ fun MainView(
214
220
viewModel.maybeRequestVpnPermission()
215
221
LaunchVpnPermissionIfNeeded (viewModel)
216
222
LaunchedEffect (state) {
217
- if (state == Ipn .State .Running && ! AndroidTVUtil . isAndroidTV()) {
218
- viewModel.showDirectoryPickerLauncher ()
223
+ if (state == Ipn .State .Running && ! isAndroidTV()) {
224
+ viewModel.checkIfTaildropDirectorySelected ()
219
225
}
220
226
}
221
227
@@ -248,13 +254,29 @@ fun MainView(
248
254
{ viewModel.login() },
249
255
loginAtUrl,
250
256
netmap?.SelfNode ,
251
- {
252
- viewModel.showVPNPermissionLauncherIfUnauthorized()
253
- })
257
+ { viewModel.showVPNPermissionLauncherIfUnauthorized() })
254
258
}
255
259
}
256
- }
257
260
261
+ showDirectoryPickerInterstitial.let { show ->
262
+ if (show) {
263
+ AppTheme {
264
+ AlertDialog (
265
+ onDismissRequest = { viewModel.showDirectoryPickerLauncher() },
266
+ title = {
267
+ Text (text = stringResource(id = R .string.taildrop_directory_picker_title))
268
+ },
269
+ text = { TaildropDirectoryPickerPrompt () },
270
+ confirmButton = {
271
+ PrimaryActionButton (onClick = { viewModel.showDirectoryPickerLauncher() }) {
272
+ Text (
273
+ text = stringResource(id = R .string.taildrop_directory_picker_button))
274
+ }
275
+ })
276
+ }
277
+ }
278
+ }
279
+ }
258
280
currentPingDevice?.let { _ ->
259
281
ModalBottomSheet (onDismissRequest = { viewModel.onPingDismissal() }) {
260
282
PingView (model = viewModel.pingViewModel)
@@ -264,6 +286,20 @@ fun MainView(
264
286
}
265
287
}
266
288
289
+ @Composable
290
+ fun TaildropDirectoryPickerPrompt () {
291
+ val uriHandler = LocalUriHandler .current
292
+
293
+ Column (verticalArrangement = Arrangement .spacedBy(8 .dp), horizontalAlignment = Alignment .Start ) {
294
+ Text (text = stringResource(id = R .string.taildrop_directory_picker_body))
295
+ Text (
296
+ text = stringResource(id = R .string.taildrop_directory_picker_info),
297
+ modifier = Modifier .clickable { uriHandler.openUri(Links .TAILDROP_KB_URL ) },
298
+ color = MaterialTheme .colorScheme.primary,
299
+ textDecoration = TextDecoration .Underline )
300
+ }
301
+ }
302
+
267
303
@Composable
268
304
fun LaunchVpnPermissionIfNeeded (viewModel : MainViewModel ) {
269
305
val lifecycleOwner = LocalLifecycleOwner .current
@@ -441,7 +477,7 @@ fun ConnectView(
441
477
loginAction : () -> Unit ,
442
478
loginAtUrlAction : (String ) -> Unit ,
443
479
selfNode : Tailcfg .Node ? ,
444
- showVPNPermissionLauncher : () -> Unit
480
+ showVPNPermissionLauncher : () -> Unit ,
445
481
) {
446
482
LaunchedEffect (isPrepared) {
447
483
if (! isPrepared && shouldStartAutomatically) {
@@ -553,7 +589,7 @@ fun PeerList(
553
589
viewModel : MainViewModel ,
554
590
onNavigateToPeerDetails : (Tailcfg .Node ) -> Unit ,
555
591
onSearchBarClick : () -> Unit ,
556
- onSearch : (String ) -> Unit
592
+ onSearch : (String ) -> Unit ,
557
593
) {
558
594
val peerList by viewModel.peers.collectAsState(initial = emptyList<PeerSet >())
559
595
val searchTermStr by viewModel.searchTerm.collectAsState(initial = " " )
@@ -774,7 +810,7 @@ fun PromptPermissionsIfNecessary() {
774
810
@Composable
775
811
fun Search (
776
812
onSearchBarClick : () -> Unit , // Callback for navigating to SearchView
777
- backgroundColor : Color = MaterialTheme .colorScheme.background // Default background color
813
+ backgroundColor : Color = MaterialTheme .colorScheme.background, // Default background color
778
814
) {
779
815
// Prevent multiple taps
780
816
var isNavigating by remember { mutableStateOf(false ) }
0 commit comments