From e0d2b762694ba69b92d3a8848af2b58a19373708 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Tue, 19 Mar 2024 18:11:26 -0700 Subject: [PATCH] [568eb59] android/ui: address preliminary design feedback Updates tailscale/corp#18202 Adds back navigation to all of the headers. Corrects all padding and some colours Adds separators to the device list Adds the Compat theme so we don't have the black top and bottom bars. Removes all of the chevrons. Other minor tweaks Signed-off-by: Jonathan Nobels --- android/src/main/AndroidManifest.xml | 1 + .../java/com/tailscale/ipn/MainActivity.kt | 16 +- .../com/tailscale/ipn/ui/view/AboutView.kt | 7 +- .../tailscale/ipn/ui/view/BugReportView.kt | 19 +- .../tailscale/ipn/ui/view/ExitNodePicker.kt | 2 +- .../ipn/ui/view/MDMSettingsDebugView.kt | 5 +- .../com/tailscale/ipn/ui/view/MainView.kt | 261 ++++++++++-------- .../tailscale/ipn/ui/view/ManagedByView.kt | 7 +- .../com/tailscale/ipn/ui/view/PeerDetails.kt | 88 +++--- .../com/tailscale/ipn/ui/view/SettingsView.kt | 8 +- .../com/tailscale/ipn/ui/view/SharedViews.kt | 18 +- .../com/tailscale/ipn/ui/view/TintedSwitch.kt | 24 ++ .../tailscale/ipn/ui/view/UserSwitcherView.kt | 4 +- .../com/tailscale/ipn/ui/view/UserView.kt | 2 +- .../ipn/ui/viewModel/SettingsViewModel.kt | 3 +- android/src/main/res/values/strings.xml | 12 +- 16 files changed, 275 insertions(+), 202 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index e87b8bb4cc..aa30b6de54 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ android:allowBackup="false" android:banner="@drawable/tv_banner" android:icon="@mipmap/ic_launcher" + android:theme="@style/Theme.AppCompat" android:label="Tailscale" android:roundIcon="@mipmap/ic_launcher_round"> +fun AboutView(nav: BackNavigation) { + Scaffold(topBar = { Header(R.string.about_view_title, onBack = nav.onBack) }) { innerPadding -> Column( verticalArrangement = Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().fillMaxHeight().safeContentPadding()) { + modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(innerPadding)) { Image( modifier = Modifier.width(100.dp) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt index f71e4ca609..c944413991 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt @@ -24,42 +24,44 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links +import com.tailscale.ipn.ui.theme.ts_color_light_blue import com.tailscale.ipn.ui.util.defaultPaddingModifier import com.tailscale.ipn.ui.util.settingsRowModifier import com.tailscale.ipn.ui.viewModel.BugReportViewModel import kotlinx.coroutines.flow.StateFlow @Composable -fun BugReportView(model: BugReportViewModel = viewModel()) { +fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) { val handler = LocalUriHandler.current - Scaffold(topBar = { Header(R.string.bug_report_title) }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).padding(8.dp).fillMaxWidth().fillMaxHeight()) { + Scaffold(topBar = { Header(R.string.bug_report_title, onBack = nav.onBack) }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).padding(24.dp).fillMaxWidth().fillMaxHeight()) { ClickableText( text = contactText(), modifier = Modifier.fillMaxWidth(), style = MaterialTheme.typography.bodyMedium, onClick = { handler.openUri(Links.SUPPORT_URL) }) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) ReportIdRow(bugReportIdFlow = model.bugReportID) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) Text( text = stringResource(id = R.string.bug_report_id_desc), @@ -87,7 +89,8 @@ fun ReportIdRow(bugReportIdFlow: StateFlow) { Text( text = bugReportId.value, style = MaterialTheme.typography.titleMedium, - maxLines = 1, + fontFamily = FontFamily.Monospace, + maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = defaultPaddingModifier()) } @@ -105,7 +108,7 @@ fun contactText(): AnnotatedString { } pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL) - withStyle(style = SpanStyle(color = Color.Blue)) { + withStyle(style = SpanStyle(color = ts_color_light_blue, textDecoration = TextDecoration.Underline)) { append(stringResource(id = R.string.bug_report_instructions_linktext)) } pop() diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt index 0be44efad0..4ebbb0c64a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt @@ -43,7 +43,7 @@ fun ExitNodePicker( model: ExitNodePickerViewModel = viewModel(factory = ExitNodePickerViewModelFactory(nav)) ) { LoadingIndicator.Wrap { - Scaffold(topBar = { Header(R.string.choose_exit_node) }) { innerPadding -> + Scaffold(topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateHome) }) { innerPadding -> val tailnetExitNodes = model.tailnetExitNodes.collectAsState() val mullvadExitNodes = model.mullvadExitNodesByCountryCode.collectAsState() val anyActive = model.anyActive.collectAsState() diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt index 191f7a97f9..ab65463c65 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt @@ -31,8 +31,9 @@ import com.tailscale.ipn.ui.viewModel.IpnViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MDMSettingsDebugView(model: IpnViewModel = viewModel()) { - Scaffold(topBar = { Header(R.string.current_mdm_settings) }) { innerPadding -> +fun MDMSettingsDebugView(nav: BackNavigation, model: IpnViewModel = viewModel()) { + Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = nav.onBack) }) { innerPadding + -> val mdmSettings = IpnViewModel.mdmSettings.collectAsState().value LazyColumn(modifier = Modifier.padding(innerPadding)) { items(enumValues()) { booleanSetting -> diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index a6f52216ec..cb6b8b842f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -10,21 +10,23 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -32,7 +34,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -68,49 +69,60 @@ data class MainViewNavigation( @Composable fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewModel()) { - Scaffold { _ -> - Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Center) { - val state = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState) - val user = viewModel.loggedInUser.collectAsState(initial = null) + Scaffold(contentWindowInsets = WindowInsets.Companion.statusBars) { paddingInsets -> + Column( + modifier = Modifier.fillMaxWidth().padding(paddingInsets), + verticalArrangement = Arrangement.Center) { + val state = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState) + val user = viewModel.loggedInUser.collectAsState(initial = null) - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically) { - val isOn = viewModel.vpnToggleState.collectAsState(initial = false) - if (state.value != Ipn.State.NeedsLogin && state.value != Ipn.State.NoState) { - Switch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value) - Spacer(Modifier.size(3.dp)) - } + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondaryContainer) + .padding(horizontal = 8.dp) + .padding(top = 10.dp), + verticalAlignment = Alignment.CenterVertically) { + val isOn = viewModel.vpnToggleState.collectAsState(initial = false) + if (state.value != Ipn.State.NeedsLogin && state.value != Ipn.State.NoState) { + TintedSwitch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value) + Spacer(Modifier.size(3.dp)) + } - StateDisplay(viewModel.stateRes, viewModel.userName) + StateDisplay(viewModel.stateRes, viewModel.userName) - Box( - modifier = Modifier.weight(1f).clickable { navigation.onNavigateToSettings() }, - contentAlignment = Alignment.CenterEnd) { - when (user.value) { - null -> SettingsButton(user.value) { navigation.onNavigateToSettings() } - else -> Avatar(profile = user.value, size = 36) - } - } - } + Box( + modifier = Modifier.weight(1f).clickable { navigation.onNavigateToSettings() }, + contentAlignment = Alignment.CenterEnd) { + when (user.value) { + null -> SettingsButton(user.value) { navigation.onNavigateToSettings() } + else -> Avatar(profile = user.value, size = 36) + } + } + } - when (state.value) { - Ipn.State.Running -> { + when (state.value) { + Ipn.State.Running -> { - val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "") - ExitNodeStatus(navAction = navigation.onNavigateToExitNodes, viewModel = viewModel) - PeerList( - searchTerm = viewModel.searchTerm, - state = viewModel.ipnState, - peers = viewModel.peers, - selfPeer = selfPeerId.value, - onNavigateToPeerDetails = navigation.onNavigateToPeerDetails, - onSearch = { viewModel.searchPeers(it) }) + val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "") + Row(modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer).padding(top = 10.dp, bottom = 20.dp)) { + ExitNodeStatus( + navAction = navigation.onNavigateToExitNodes, + viewModel = viewModel + ) + } + PeerList( + searchTerm = viewModel.searchTerm, + state = viewModel.ipnState, + peers = viewModel.peers, + selfPeer = selfPeerId.value, + onNavigateToPeerDetails = navigation.onNavigateToPeerDetails, + onSearch = { viewModel.searchPeers(it) }) + } + Ipn.State.Starting -> StartingView() + else -> ConnectView(user.value, { viewModel.toggleVpn() }, { viewModel.login {} }) + } } - Ipn.State.Starting -> StartingView() - else -> ConnectView(user.value, { viewModel.toggleVpn() }, { viewModel.login {} }) - } - } } } @@ -132,19 +144,21 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) { } Box( modifier = - Modifier.clickable { navAction() } - .padding(horizontal = 8.dp) - .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer) - .fillMaxWidth()) { - Column(modifier = Modifier.padding(6.dp)) { + Modifier + .clickable { navAction() } + .padding(horizontal = 8.dp) + .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth()) { + Column(modifier = Modifier.padding(vertical = 15.dp, horizontal = 18.dp)) { Text( text = stringResource(id = R.string.exit_node), - style = MaterialTheme.typography.titleMedium) + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleSmall) Row(verticalAlignment = Alignment.CenterVertically) { Text( text = exitNode ?: stringResource(id = R.string.none), - style = MaterialTheme.typography.bodyMedium) + style = MaterialTheme.typography.bodyLarge) Icon( Icons.Outlined.ArrowDropDown, null, @@ -207,63 +221,77 @@ fun StartingView() { @Composable fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - Column( - modifier = - Modifier.background(MaterialTheme.colorScheme.secondaryContainer) - .padding(8.dp) - .fillMaxWidth(0.7f) - .fillMaxHeight(), - verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (user != null && !user.isEmpty()) { - Icon( - painter = painterResource(id = R.drawable.power), - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.not_connected), - fontSize = MaterialTheme.typography.titleMedium.fontSize, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center, - fontFamily = MaterialTheme.typography.titleMedium.fontFamily) - val tailnetName = user.NetworkProfile?.DomainName ?: "" - Text( - stringResource(id = R.string.connect_to_tailnet, tailnetName), - fontSize = MaterialTheme.typography.titleMedium.fontSize, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.secondary, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.size(1.dp)) - PrimaryActionButton(onClick = connectAction) { - Text( - text = stringResource(id = R.string.connect), - fontSize = MaterialTheme.typography.titleMedium.fontSize) - } - } else { - TailscaleLogoView(Modifier.size(50.dp)) - Spacer(modifier = Modifier.size(1.dp)) - Text( - text = stringResource(id = R.string.welcome_to_tailscale), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center) - Text( - stringResource(R.string.login_to_join_your_tailnet), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, - textAlign = TextAlign.Center) - Spacer(modifier = Modifier.size(1.dp)) - PrimaryActionButton(onClick = loginAction) { - Text( - text = stringResource(id = R.string.log_in), - fontSize = MaterialTheme.typography.titleMedium.fontSize) - } + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = + Modifier + .background(MaterialTheme.colorScheme.secondaryContainer) + .fillMaxWidth()) { + Column( + modifier = + Modifier + .padding(8.dp) + .fillMaxWidth(0.7f) + .fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy( + 8.dp, + alignment = Alignment.CenterVertically + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (user != null && !user.isEmpty()) { + Icon( + painter = painterResource(id = R.drawable.power), + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(id = R.string.not_connected), + fontSize = MaterialTheme.typography.titleMedium.fontSize, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + fontFamily = MaterialTheme.typography.titleMedium.fontFamily + ) + val tailnetName = user.NetworkProfile?.DomainName ?: "" + Text( + stringResource(id = R.string.connect_to_tailnet, tailnetName), + fontSize = MaterialTheme.typography.titleMedium.fontSize, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.size(1.dp)) + PrimaryActionButton(onClick = connectAction) { + Text( + text = stringResource(id = R.string.connect), + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) + } + } else { + TailscaleLogoView(Modifier.size(50.dp)) + Spacer(modifier = Modifier.size(1.dp)) + Text( + text = stringResource(id = R.string.welcome_to_tailscale), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center + ) + Text( + stringResource(R.string.login_to_join_your_tailnet), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.size(1.dp)) + PrimaryActionButton(onClick = loginAction) { + Text( + text = stringResource(id = R.string.log_in), + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) + } + } + } } - } } } @@ -308,9 +336,9 @@ fun PeerList( trailingIcon = { if (searchTermStr.isNotEmpty()) ClearButton({ onSearch("") }) else CloseButton() }, - tonalElevation = 2.dp, - shadowElevation = 2.dp, - colors = SearchBarDefaults.colors(), + tonalElevation = 0.dp, + shadowElevation = 0.dp, + colors = SearchBarDefaults.colors(containerColor = Color.Transparent, dividerColor = Color.Transparent), modifier = Modifier.fillMaxWidth()) { LazyColumn( modifier = @@ -323,7 +351,9 @@ fun PeerList( Text( text = peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user), - style = MaterialTheme.typography.titleLarge) + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) }) } peerSet.peers.forEach { peer -> @@ -344,19 +374,22 @@ fun PeerList( } Box( modifier = - Modifier.size(8.dp) - .background( - color = color, shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) + Modifier + .size(10.dp) + .background( + color = color, shape = RoundedCornerShape(percent = 50) + )) {} + Spacer(modifier = Modifier.size(6.dp)) Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) } }, supportingContent = { Text( text = peer.Addresses?.first()?.split("/")?.first() ?: "", - style = MaterialTheme.typography.bodyMedium) - }, - trailingContent = { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null) }) + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary) + }) + HorizontalDivider(color = MaterialTheme.colorScheme.secondaryContainer) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt index 147a06b95c..2b8b108e71 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt @@ -7,8 +7,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.safeContentPadding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -22,8 +21,8 @@ import com.tailscale.ipn.mdm.StringSetting import com.tailscale.ipn.ui.viewModel.IpnViewModel @Composable -fun ManagedByView(model: IpnViewModel = viewModel()) { - Surface(color = MaterialTheme.colorScheme.surface) { +fun ManagedByView(nav: BackNavigation, model: IpnViewModel = viewModel()) { + Scaffold(topBar = { Header(R.string.managed_by, onBack = nav.onBack) }) { innerPadding -> Column( verticalArrangement = Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically), diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt index be770cf5bb..8e21494a3f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt @@ -35,52 +35,51 @@ import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory @Composable fun PeerDetails( + nav: BackNavigation, nodeId: String, model: PeerDetailsViewModel = viewModel(factory = PeerDetailsViewModelFactory(nodeId)) ) { - Scaffold( - topBar = { - Column( - modifier = Modifier.fillMaxWidth().padding(8.dp), - ) { - Text( - text = model.nodeName, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary) - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = - Modifier.size(8.dp) - .background( - color = model.connectedColor, - shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(id = model.connectedStrRes), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary) - } - } - }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxHeight()) { - Text( - text = stringResource(id = R.string.addresses_section), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary) + Scaffold(topBar = { Header(title = R.string.peer_details, onBack = nav.onBack) }) { innerPadding + -> + Column( + modifier = Modifier.fillMaxWidth().padding(innerPadding).padding(horizontal = 16.dp).padding(top = 22.dp), + ) { + Text( + text = model.nodeName, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary) + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.size(8.dp) + .background( + color = model.connectedColor, shape = RoundedCornerShape(percent = 50))) {} + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(id = model.connectedStrRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary) + } + Column(modifier = Modifier.fillMaxHeight()) { + Text( + text = stringResource(id = R.string.addresses_section), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary) - Column(modifier = settingsRowModifier()) { - model.addresses.forEach { AddressRow(address = it.address, type = it.typeString) } - } + Column(modifier = settingsRowModifier()) { + model.addresses.forEach { AddressRow(address = it.address, type = it.typeString) } + } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) - Column(modifier = settingsRowModifier()) { - model.info.forEach { - ValueRow(title = stringResource(id = it.titleRes), value = it.value.getString()) - } + Column(modifier = settingsRowModifier()) { + model.info.forEach { + ValueRow(title = stringResource(id = it.titleRes), value = it.value.getString()) } } } + } + } } @Composable @@ -88,25 +87,26 @@ fun AddressRow(address: String, type: String) { val localClipboardManager = LocalClipboardManager.current Row( + verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + Modifier.padding(horizontal = 8.dp, vertical = 8.dp) .clickable(onClick = { localClipboardManager.setText(AnnotatedString(address)) })) { Column { - Text(text = address, style = MaterialTheme.typography.titleMedium) - Text(text = type, style = MaterialTheme.typography.bodyMedium) + Text(text = address) + Text(text = type, fontSize = MaterialTheme.typography.labelLarge.fontSize, color = MaterialTheme.colorScheme.secondary) } Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Icon(Icons.Outlined.Share, null) + Icon(Icons.Outlined.Share, null, tint = MaterialTheme.colorScheme.secondary) } } } @Composable fun ValueRow(title: String, value: String) { - Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).fillMaxWidth()) { - Text(text = title, style = MaterialTheme.typography.titleMedium) + Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp).fillMaxWidth()) { + Text(text = title) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Text(text = value, style = MaterialTheme.typography.bodyMedium) + Text(text = value, color = MaterialTheme.colorScheme.secondary) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index 8f76556d10..b104572cfc 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -51,8 +50,8 @@ fun Settings( val user = viewModel.loggedInUser.collectAsState().value val isAdmin = viewModel.isAdmin.collectAsState().value - Scaffold(topBar = { Header(title = R.string.settings_title) }) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding).fillMaxHeight()) { + Scaffold(topBar = { Header(title = R.string.settings_title, onBack = settingsNav.onBackPressed) }) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).fillMaxHeight().padding(16.dp)) { UserView( profile = user, actionState = UserActionState.NAV, @@ -142,7 +141,7 @@ fun SettingRow(setting: Setting) { SettingType.SWITCH -> { Text(setting.title.getString()) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Switch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled) + TintedSwitch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled) } } SettingType.NAV -> { @@ -155,7 +154,6 @@ fun SettingRow(setting: Setting) { Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { Text(text = txtVal, style = MaterialTheme.typography.bodyMedium) } - ChevronRight() } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt index 362424086a..2706b4232b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt @@ -4,9 +4,11 @@ package com.tailscale.ipn.ui.view import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -20,22 +22,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +data class BackNavigation( + val onBack: () -> Unit, +) + // Header view for all secondary screens @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Header(@StringRes title: Int) { +fun Header(@StringRes title: Int, onBack: (() -> Unit)? = null) { TopAppBar( colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceContainer, titleContentColor = MaterialTheme.colorScheme.primary, ), - title = { Text(stringResource(title)) }) + title = { Text(stringResource(title)) }, + navigationIcon = { onBack?.let { BackArrow(action = it) } }, + ) } @Composable -fun ChevronRight() { - Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null) +fun BackArrow(action: () -> Unit) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null, modifier = Modifier.clickable { action() }.padding(start = 15.dp, end = 20.dp)) } @Composable diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt new file mode 100644 index 0000000000..ea2a89edce --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt @@ -0,0 +1,24 @@ +package com.tailscale.ipn.ui.view + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import com.tailscale.ipn.ui.theme.ts_color_light_blue + +@Composable +fun TintedSwitch(checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + enabled: Boolean = true) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + colors = SwitchDefaults.colors( + checkedBorderColor = ts_color_light_blue, + checkedThumbColor = ts_color_light_blue, + checkedTrackColor = ts_color_light_blue.copy(alpha = 0.3f), + uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) +} \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt index e0af5702d7..5cdf600c5c 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt @@ -25,12 +25,12 @@ import com.tailscale.ipn.ui.viewModel.UserSwitcherViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun UserSwitcherView(viewModel: UserSwitcherViewModel = viewModel()) { +fun UserSwitcherView(nav: BackNavigation, viewModel: UserSwitcherViewModel = viewModel()) { val users = viewModel.loginProfiles.collectAsState().value val currentUser = viewModel.loggedInUser.collectAsState().value - Scaffold(topBar = { Header(R.string.accounts) }) { innerPadding -> + Scaffold(topBar = { Header(R.string.accounts, onBack = nav.onBack) }) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt index 646ed8ab6b..7ec3690348 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt @@ -62,7 +62,7 @@ fun UserView( when (actionState) { UserActionState.CURRENT -> CheckedIndicator() UserActionState.SWITCHING -> SimpleActivityIndicator(size = 26) - UserActionState.NAV -> ChevronRight() + UserActionState.NAV -> Unit UserActionState.NONE -> Unit } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt index 9e1ccf5942..77f85b2ee7 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt @@ -78,7 +78,8 @@ data class SettingsNav( val onNavigateToAbout: () -> Unit, val onNavigateToMDMSettings: () -> Unit, val onNavigateToManagedBy: () -> Unit, - val onNavigateToUserSwitcher: () -> Unit + val onNavigateToUserSwitcher: () -> Unit, + val onBackPressed: () -> Unit, ) class SettingsViewModelFactory(private val navigation: SettingsNav) : ViewModelProvider.Factory { diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 3fe5c6b339..ad5d193c5a 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -24,13 +24,14 @@ Terms of Service WireGuard is a registered trademark of Jason A. Donenfeld.\n\n© 2024 Tailscale Inc. All rights reserved.\nTailscale is a registered trademark of Tailscale Inc. The Tailscale App Icon + Managed By Report a Bug To report a bug,  - contact our support team  - and include the ID below. - This ID helps us find the event ino our diagnostic logs. This process does not share any of your personally-identifiable information. + contact our support team  + and include the ID below. + This ID helps us find the event in our diagnostic logs. This process does not share any of your personally-identifiable information. Settings @@ -41,16 +42,17 @@ Use Tailscale DNS - Exit Node + EXIT NODE Starting… "Connect again to talk to the other devices in the %1$s tailnet." Welcome to Tailscale Log in to join your tailnet and connect your devices. - TAILSCALE ADDRESSES + OS Key Expiry + Tailscale Addresses Current MDM Settings