From 910511d83859900ac23da317ee3a22b6fa947e78 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Fri, 22 Mar 2024 11:49:42 -0400 Subject: [PATCH] [568eb59] android/ui: address preliminary design feedback (#227) 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 Co-authored-by: James Tucker --- 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 e87b8bb..aa30b6d 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 f71e4ca..3b5ecad 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 0be44ef..d291a18 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 191f7a9..ab65463 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 a6f5221..699937f 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 147a06b..2b8b108 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 be770cf..c601905 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 8f76556..ef5fbf0 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 3624240..02af82c 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 0000000..69f1ccb --- /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 e0af570..5cdf600 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 646ed8a..7ec3690 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 9e1ccf5..77f85b2 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 3fe5c6b..ad5d193 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