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.