diff --git a/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt b/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt index cf59a29..38213cc 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/theme/Theme.kt @@ -35,8 +35,7 @@ fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp, lineHeight = 26.sp), // bodyMedium is styled to use same line height as titleMedium to ensure even vertical // margins in list items. - bodyMedium = - MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp, lineHeight = 26.sp)) + bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp)) val systemUiController = rememberSystemUiController() @@ -132,6 +131,23 @@ val ColorScheme.listItem: ListItemColors disabledTrailingIconColor = default.disabledTrailingIconColor) } +/** Like listItem, but with the overline content using the onSurface color. */ +val ColorScheme.titledListItem: ListItemColors + @Composable + get() { + val default = listItem + return ListItemColors( + containerColor = default.containerColor, + headlineColor = default.headlineColor, + leadingIconColor = default.leadingIconColor, + overlineColor = MaterialTheme.colorScheme.onSurface, + supportingTextColor = default.supportingTextColor, + trailingIconColor = default.trailingIconColor, + disabledHeadlineColor = default.disabledHeadlineColor, + disabledLeadingIconColor = default.disabledLeadingIconColor, + disabledTrailingIconColor = default.disabledTrailingIconColor) + } + /** Color scheme for disabled list items. */ val ColorScheme.disabledListItem: ListItemColors @Composable diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt index e12e3b2..ef1b7ce 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt @@ -18,10 +18,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.tailscale.ipn.R -import com.tailscale.ipn.ui.theme.listItem +import com.tailscale.ipn.ui.theme.titledListItem @Composable fun ClipboardValueView( @@ -32,16 +31,11 @@ fun ClipboardValueView( ) { val localClipboardManager = LocalClipboardManager.current ListItem( - colors = MaterialTheme.colorScheme.listItem, + colors = MaterialTheme.colorScheme.titledListItem, modifier = Modifier.clickable { localClipboardManager.setText(AnnotatedString(value)) }, overlineContent = { title?.let { Text(it, style = MaterialTheme.typography.titleMedium) } }, headlineContent = { - Text( - text = value, - style = MaterialTheme.typography.bodyMedium, - fontFamily = fontFamily, - maxLines = 2, - overflow = TextOverflow.Ellipsis) + Text(text = value, style = MaterialTheme.typography.bodyMedium, fontFamily = fontFamily) }, supportingContent = { subtitle?.let { subtitle -> diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt b/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt index 4c76a4b..647e6fe 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt @@ -3,31 +3,55 @@ package com.tailscale.ipn.ui.util +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp object Lists { @Composable fun SectionDivider(title: String? = null) { Box(Modifier.size(0.dp, 16.dp)) - title?.let { - ListItem(headlineContent = { Text(title, style = MaterialTheme.typography.titleMedium) }) - } + title?.let { SectionTitle(title) } } @Composable fun ItemDivider() { HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } + + @Composable + fun SectionTitle( + title: String, + bottomPadding: Dp = 0.dp, + style: TextStyle = MaterialTheme.typography.titleMedium, + fontWeight: FontWeight? = null + ) { + Box( + modifier = + Modifier.fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface, shape = RectangleShape)) { + Text( + title, + modifier = + Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = bottomPadding), + style = style, + fontWeight = fontWeight) + } + } } /** Similar to items() but includes a horizontal divider between items. */ 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 3be36b4..46da2b2 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 @@ -11,7 +11,6 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier @@ -20,7 +19,6 @@ 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.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.lifecycle.viewmodel.compose.viewModel @@ -39,18 +37,16 @@ fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) Column(modifier = Modifier.padding(innerPadding).fillMaxWidth().fillMaxHeight()) { ListItem( headlineContent = { - ClickableText(text = contactText(), onClick = { handler.openUri(Links.SUPPORT_URL) }) + ClickableText( + text = contactText(), + style = MaterialTheme.typography.bodyMedium, + onClick = { handler.openUri(Links.SUPPORT_URL) }) }) - ClipboardValueView(bugReportID, title = stringResource(R.string.bug_report_id)) - - ListItem( - headlineContent = { - Text( - text = stringResource(id = R.string.bug_report_id_desc), - textAlign = TextAlign.Left, - style = MaterialTheme.typography.bodySmall) - }) + ClipboardValueView( + bugReportID, + title = stringResource(R.string.bug_report_id), + subtitle = stringResource(id = R.string.bug_report_id_desc)) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/DNSSettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/DNSSettingsView.kt index 0c18207..b2b9217 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/DNSSettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/DNSSettingsView.kt @@ -43,7 +43,10 @@ fun DNSSettingsView( LazyColumn(Modifier.padding(innerPadding)) { item("state") { FeatureStateView(state) } - item("toggle") { SettingRow(model.useDNSSetting) } + item("toggle") { + Lists.SectionDivider() + SettingRow(model.useDNSSetting) + } if (resolvers.isNotEmpty()) { item("resolversHeader") { Lists.SectionDivider(stringResource(R.string.resolvers)) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/FeatureStateView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/FeatureStateView.kt index 6affe18..4eeeffd 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/FeatureStateView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/FeatureStateView.kt @@ -25,7 +25,7 @@ fun FeatureStateView(state: FeatureStateRepresentation) { painter = painterResource(state.symbolDrawable), contentDescription = null, tint = state.tint, - modifier = Modifier.size(64.dp)) + modifier = Modifier.size(36.dp)) }, headlineContent = { Text(stringResource(state.title), style = MaterialTheme.typography.titleMedium) 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 a70bd55..ab33b8b 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 @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars @@ -185,7 +184,7 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = - location?.let { "${it.CountryCode?.flag()} ${it.Country} - ${it.City}" } + location?.let { "${it.CountryCode?.flag()} ${it.Country}: ${it.City}" } ?: name ?: stringResource(id = R.string.none), style = MaterialTheme.typography.bodyMedium, @@ -320,9 +319,9 @@ fun PeerList( Box(modifier = Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surface)) { OutlinedTextField( modifier = - Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).onFocusChanged { - isFocussed = it.isFocused - }, + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 0.dp) + .onFocusChanged { isFocussed = it.isFocused }, singleLine = true, shape = MaterialTheme.shapes.large, colors = MaterialTheme.colorScheme.searchBarColors, @@ -354,54 +353,54 @@ fun PeerList( } LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - var first = true - peerList.value.forEach { peerSet -> - if (!first) { - item(key = "spacer_${peerSet.user?.DisplayName}") { - Lists.ItemDivider() - Spacer(Modifier.height(24.dp)) - } - } - first = false + modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colorScheme.surface)) { + var first = true + peerList.value.forEach { peerSet -> + if (!first) { + item(key = "user_divider_${peerSet.user?.ID ?: 0L}") { Lists.ItemDivider() } + } + first = false - stickyHeader { - ListItem( - modifier = Modifier.heightIn(max = 48.dp), - headlineContent = { - Text( - text = peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold) - }) - } + stickyHeader { + Spacer( + Modifier.height(16.dp) + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.surface)) - itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> - ListItem( - modifier = Modifier.clickable { onNavigateToPeerDetails(peer) }, - colors = MaterialTheme.colorScheme.listItem, - headlineContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = - Modifier.padding(top = 2.dp) - .size(10.dp) - .background( - color = peer.connectedColor(netmap.value), - shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) - Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) - } - }, - supportingContent = { - Text( - text = peer.Addresses?.first()?.split("/")?.first() ?: "", - style = MaterialTheme.typography.bodyMedium) - }) + Lists.SectionTitle( + peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user), + bottomPadding = 8.dp, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold) + } + + itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> + ListItem( + modifier = Modifier.clickable { onNavigateToPeerDetails(peer) }, + colors = MaterialTheme.colorScheme.listItem, + headlineContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.padding(top = 2.dp) + .size(10.dp) + .background( + color = peer.connectedColor(netmap.value), + shape = RoundedCornerShape(percent = 50))) {} + Spacer(modifier = Modifier.size(8.dp)) + Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) + } + }, + supportingContent = { + Text( + text = peer.Addresses?.first()?.split("/")?.first() ?: "", + style = + MaterialTheme.typography.bodyMedium.copy( + lineHeight = MaterialTheme.typography.titleMedium.lineHeight)) + }) + } + } } - } - } } @OptIn(ExperimentalPermissionsApi::class) 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 3b04052..22bb698 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,7 +35,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tailscale.ipn.R import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.short -import com.tailscale.ipn.ui.theme.ts_color_light_blue import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel @@ -121,7 +120,7 @@ fun AddressRow(address: String, type: String) { supportingContent = { Text(text = type) }, trailingContent = { // TODO: there is some overlap with other uses of clipboard, DRY - Icon(painter = painterResource(id = R.drawable.clipboard), null, tint = ts_color_light_blue) + Icon(painter = painterResource(id = R.drawable.clipboard), null) }) } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt index 353f48d..fb593b4 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PermissionsView.kt @@ -7,9 +7,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -17,6 +14,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -24,6 +22,7 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.Permissions +import com.tailscale.ipn.ui.theme.success import com.tailscale.ipn.ui.util.itemsWithDividers @OptIn(ExperimentalPermissionsApi::class) @@ -46,7 +45,11 @@ fun PermissionsView(nav: BackNavigation, openApplicationSettings: () -> Unit) { modifier = modifier, leadingContent = { Icon( - if (state.status.isGranted) Icons.Filled.CheckCircle else Icons.Filled.Warning, + if (state.status.isGranted) painterResource(R.drawable.check_circle) + else painterResource(R.drawable.xmark_circle), + tint = + if (state.status.isGranted) MaterialTheme.colorScheme.success + else MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp), contentDescription = stringResource(if (state.status.isGranted) R.string.ok else R.string.warning)) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt index 94591e5..6c06165 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -3,9 +3,8 @@ package com.tailscale.ipn.ui.view -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.ClickableText @@ -29,7 +28,7 @@ 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.theme.link import com.tailscale.ipn.ui.util.ClipboardValueView import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.util.LoadingIndicator @@ -48,12 +47,11 @@ fun TailnetLockSetupView( Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = nav.onBack) }) { innerPadding -> LoadingIndicator.Wrap { LazyColumn(modifier = Modifier.padding(innerPadding)) { - item(key = "header") { - ExplainerView() - Spacer(Modifier.size(4.dp)) - } + item(key = "header") { ExplainerView() } items(items = statusItems, key = { "status_${it.title}" }) { statusItem -> + Lists.ItemDivider() + ListItem( leadingContent = { Icon( @@ -74,6 +72,8 @@ fun TailnetLockSetupView( } item(key = "tailnetLockKey") { + Lists.SectionDivider() + ClipboardValueView( value = tailnetLockKey, title = stringResource(R.string.tailnet_lock_key), @@ -88,22 +88,29 @@ fun TailnetLockSetupView( private fun ExplainerView() { val handler = LocalUriHandler.current - ClickableText( - explainerText(), - modifier = Modifier.padding(16.dp), - onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }) + ListItem( + headlineContent = { + Box(modifier = Modifier.padding(vertical = 8.dp)) { + ClickableText( + explainerText(), + onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) }, + style = MaterialTheme.typography.bodyMedium) + } + }) } @Composable fun explainerText(): AnnotatedString { val annotatedString = buildAnnotatedString { - withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append(stringResource(id = R.string.tailnet_lock_explainer)) - } + append(stringResource(id = R.string.tailnet_lock_explainer)) pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL) + withStyle( - style = SpanStyle(color = ts_color_light_blue, textDecoration = TextDecoration.Underline)) { + style = + SpanStyle( + color = MaterialTheme.colorScheme.link, + textDecoration = TextDecoration.Underline)) { append(stringResource(id = R.string.learn_more)) } pop() diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 0c7c51a..404bc12 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -117,7 +117,7 @@ Tailnet lock - Tailnet lock lets devices in your network verify public keys distributed by the coordination server before trusting them for connectivity. + "Tailnet lock lets devices in your network verify public keys distributed by the coordination server before trusting them for connectivity. " Tailnet lock is currently enabled. Tailnet lock is currently not enabled. This node has been signed by another device. @@ -139,7 +139,7 @@ Using Tailscale DNS This device is using Tailscale to resolve DNS names. Resolvers - Search somains + Search domains Not running Tailscale is not running. This device is using the system\'s DNS resolver. This device is using the system DNS resolver.