android/ui: more styling tweaks

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
pull/277/head
Percy Wegmann 2 months ago committed by Percy Wegmann
parent 91c1a8d0f3
commit d332ce049e

@ -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

@ -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 ->

@ -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. */

@ -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))
}
}
}

@ -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)) }

@ -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)

@ -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)

@ -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)
})
}

@ -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))

@ -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()

@ -117,7 +117,7 @@
<!-- Strings for the tailnet lock screen -->
<string name="tailnet_lock">Tailnet lock</string>
<string name="tailnet_lock_explainer">Tailnet lock lets devices in your network verify public keys distributed by the coordination server before trusting them for connectivity. </string>
<string name="tailnet_lock_explainer">"Tailnet lock lets devices in your network verify public keys distributed by the coordination server before trusting them for connectivity. "</string>
<string name="tailnet_lock_enabled">Tailnet lock is currently enabled.</string>
<string name="tailnet_lock_disabled">Tailnet lock is currently not enabled.</string>
<string name="this_node_has_been_signed">This node has been signed by another device.</string>
@ -139,7 +139,7 @@
<string name="using_tailscale_dns">Using Tailscale DNS</string>
<string name="this_device_is_using_tailscale_to_resolve_dns_names">This device is using Tailscale to resolve DNS names.</string>
<string name="resolvers">Resolvers</string>
<string name="search_domains">Search somains</string>
<string name="search_domains">Search domains</string>
<string name="not_running">Not running</string>
<string name="tailscale_is_not_running_this_device_is_using_the_system_dns_resolver">Tailscale is not running. This device is using the system\'s DNS resolver.</string>
<string name="this_device_is_using_the_system_dns_resolver">This device is using the system DNS resolver.</string>

Loading…
Cancel
Save