From e953b1918932897210ff811979d324b143ca833a Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Fri, 22 Mar 2024 14:51:33 -0400 Subject: [PATCH] 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 | 51 ++-- .../tailscale/ipn/ui/view/ExitNodePicker.kt | 3 +- .../ipn/ui/view/MDMSettingsDebugView.kt | 5 +- .../com/tailscale/ipn/ui/view/MainView.kt | 232 ++++++++++-------- .../tailscale/ipn/ui/view/ManagedByView.kt | 7 +- .../com/tailscale/ipn/ui/view/PeerDetails.kt | 95 +++---- .../com/tailscale/ipn/ui/view/SettingsView.kt | 44 ++-- .../com/tailscale/ipn/ui/view/SharedViews.kt | 21 +- .../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, 301 insertions(+), 226 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..3b5ecad28c 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,50 +24,53 @@ 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()) { - ClickableText( - text = contactText(), - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyMedium, - onClick = { handler.openUri(Links.SUPPORT_URL) }) + 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) + 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), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Left, - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.bodySmall) - } + Text( + text = stringResource(id = R.string.bug_report_id_desc), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Left, + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodySmall) + } } } @@ -87,7 +90,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,9 +109,10 @@ fun contactText(): AnnotatedString { } pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL) - withStyle(style = SpanStyle(color = Color.Blue)) { - append(stringResource(id = R.string.bug_report_instructions_linktext)) - } + withStyle( + style = SpanStyle(color = ts_color_light_blue, textDecoration = TextDecoration.Underline)) { + append(stringResource(id = R.string.bug_report_instructions_linktext)) + } pop() withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { 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..d291a180ef 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,8 @@ 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..699937fdf0 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,61 @@ 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 {} }) - } - } } } @@ -135,16 +148,17 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) { Modifier.clickable { navAction() } .padding(horizontal = 8.dp) .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer) + .background(MaterialTheme.colorScheme.background) .fillMaxWidth()) { - Column(modifier = Modifier.padding(6.dp)) { + 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, @@ -208,62 +222,64 @@ fun StartingView() { 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) + 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 +324,11 @@ 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 +341,8 @@ 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 +363,20 @@ fun PeerList( } Box( modifier = - Modifier.size(8.dp) + Modifier.size(10.dp) .background( color = color, shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) + 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..c601905058 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,55 @@ 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 +91,29 @@ 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..ef5fbf050d 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,29 +50,31 @@ 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()) { - UserView( - profile = user, - actionState = UserActionState.NAV, - onClick = viewModel.navigation.onNavigateToUserSwitcher) - if (isAdmin) { - Spacer(modifier = Modifier.height(4.dp)) - AdminTextView { handler.openUri(Links.ADMIN_URL) } - } + 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, + onClick = viewModel.navigation.onNavigateToUserSwitcher) + if (isAdmin) { + Spacer(modifier = Modifier.height(4.dp)) + AdminTextView { handler.openUri(Links.ADMIN_URL) } + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - val settings = viewModel.settings.collectAsState().value - settings.forEach { settingBundle -> - Column(modifier = settingsRowModifier()) { - settingBundle.title?.let { SettingTitle(it) } - settingBundle.settings.forEach { SettingRow(it) } + val settings = viewModel.settings.collectAsState().value + settings.forEach { settingBundle -> + Column(modifier = settingsRowModifier()) { + settingBundle.title?.let { SettingTitle(it) } + settingBundle.settings.forEach { SettingRow(it) } + } + Spacer(modifier = Modifier.height(8.dp)) + } } - Spacer(modifier = Modifier.height(8.dp)) } - } - } } @Composable @@ -142,7 +143,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 +156,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..02af82c50e 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,31 @@ 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..69f1ccba16 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt @@ -0,0 +1,24 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +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)) +} 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