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 95b4f02..f71b608 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
@@ -141,6 +141,23 @@ val ColorScheme.listItem: ListItemColors
disabledTrailingIconColor = default.disabledTrailingIconColor)
}
+/** Color scheme for disabled list items. */
+val ColorScheme.disabledListItem: ListItemColors
+ @Composable
+ get() {
+ val default = ListItemDefaults.colors()
+ return ListItemColors(
+ containerColor = default.containerColor,
+ headlineColor = MaterialTheme.colorScheme.disabled,
+ leadingIconColor = default.leadingIconColor,
+ overlineColor = default.overlineColor,
+ supportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ trailingIconColor = default.trailingIconColor,
+ disabledHeadlineColor = default.disabledHeadlineColor,
+ disabledLeadingIconColor = default.disabledLeadingIconColor,
+ disabledTrailingIconColor = default.disabledTrailingIconColor)
+ }
+
/** Color scheme for list items that should be styled as a surface container. */
val ColorScheme.surfaceContainerListItem: ListItemColors
@Composable
@@ -197,3 +214,6 @@ val ColorScheme.secondaryButton: ButtonColors
disabledContainerColor = defaults.disabledContainerColor,
disabledContentColor = defaults.disabledContentColor)
}
+
+val ColorScheme.disabled: Color
+ get() = Color(0xFFAFACAB) // gray-400
diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ClickableOrGrayedOut.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ClickableOrGrayedOut.kt
deleted file mode 100644
index 1bff49b..0000000
--- a/android/src/main/java/com/tailscale/ipn/ui/util/ClickableOrGrayedOut.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package com.tailscale.ipn.ui.util
-
-import androidx.compose.foundation.clickable
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.semantics.Role
-
-/**
- * Similar to Modifier.clickable, but if enabled == false, this adds a 75% alpha to make disabled
- * items appear grayed out.
- */
-@Composable
-fun Modifier.clickableOrGrayedOut(
- enabled: Boolean = true,
- onClickLabel: String? = null,
- role: Role? = null,
- onClick: () -> Unit
-) =
- if (enabled) {
- clickable(onClickLabel = onClickLabel, role = role, onClick = onClick)
- } else {
- alpha(0.75f)
- }
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 e374da4..6e0735a 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
@@ -23,9 +23,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
+import com.tailscale.ipn.ui.theme.disabledListItem
+import com.tailscale.ipn.ui.theme.listItem
import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.LoadingIndicator
-import com.tailscale.ipn.ui.util.clickableOrGrayedOut
import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel
@@ -39,45 +40,42 @@ fun ExitNodePicker(
model: ExitNodePickerViewModel = viewModel(factory = ExitNodePickerViewModelFactory(nav))
) {
LoadingIndicator.Wrap {
- Scaffold(topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateBack) }) {
- innerPadding ->
- val tailnetExitNodes = model.tailnetExitNodes.collectAsState().value
- val mullvadExitNodesByCountryCode = model.mullvadExitNodesByCountryCode.collectAsState().value
- val mullvadExitNodeCount = model.mullvadExitNodeCount.collectAsState().value
- val anyActive = model.anyActive.collectAsState()
+ Scaffold(
+ topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateBack) },
+ bottomBar = { SettingRow(model.allowLANAccessSetting) }) { innerPadding ->
+ val tailnetExitNodes = model.tailnetExitNodes.collectAsState().value
+ val mullvadExitNodesByCountryCode =
+ model.mullvadExitNodesByCountryCode.collectAsState().value
+ val mullvadExitNodeCount = model.mullvadExitNodeCount.collectAsState().value
+ val anyActive = model.anyActive.collectAsState()
- LazyColumn(modifier = Modifier.padding(innerPadding)) {
- stickyHeader {
- RunAsExitNodeItem(nav = nav, viewModel = model)
- Lists.ItemDivider()
- SettingRow(model.allowLANAccessSetting)
- }
-
- item(key = "none") {
- Lists.SectionDivider()
-
- ExitNodeItem(
- model,
- ExitNodePickerViewModel.ExitNode(
- label = stringResource(R.string.none),
- online = true,
- selected = !anyActive.value,
- ))
- }
-
- item { Lists.ItemDivider() }
+ LazyColumn(modifier = Modifier.padding(innerPadding)) {
+ stickyHeader {
+ ExitNodeItem(
+ model,
+ ExitNodePickerViewModel.ExitNode(
+ label = stringResource(R.string.none),
+ online = true,
+ selected = !anyActive.value,
+ ))
+ Lists.ItemDivider()
+ RunAsExitNodeItem(nav = nav, viewModel = model)
+ }
- itemsWithDividers(tailnetExitNodes, key = { it.id!! }) { node -> ExitNodeItem(model, node) }
+ item(key = "divider1") { Lists.SectionDivider() }
- item { Lists.SectionDivider() }
+ itemsWithDividers(tailnetExitNodes, key = { it.id!! }) { node ->
+ ExitNodeItem(model, node)
+ }
- if (mullvadExitNodeCount > 0) {
- item(key = "mullvad") {
- MullvadItem(nav, mullvadExitNodeCount, mullvadExitNodesByCountryCode.selected)
+ if (mullvadExitNodeCount > 0) {
+ item(key = "mullvad") {
+ Lists.SectionDivider()
+ MullvadItem(nav, mullvadExitNodeCount, mullvadExitNodesByCountryCode.selected)
+ }
+ }
}
}
- }
- }
}
}
@@ -87,16 +85,17 @@ fun ExitNodeItem(
node: ExitNodePickerViewModel.ExitNode,
) {
Box {
- // TODO: add disabled styling
+ var modifier: Modifier = Modifier
+ if (node.online) {
+ modifier = modifier.clickable { viewModel.setExitNode(node) }
+ }
ListItem(
- modifier =
- Modifier.clickableOrGrayedOut(enabled = node.online) { viewModel.setExitNode(node) },
+ modifier = modifier,
+ colors =
+ if (node.online) MaterialTheme.colorScheme.listItem
+ else MaterialTheme.colorScheme.disabledListItem,
headlineContent = {
- Text(
- node.city.ifEmpty { node.label },
- style =
- if (node.online) MaterialTheme.typography.titleMedium
- else MaterialTheme.typography.bodyMedium)
+ Text(node.city.ifEmpty { node.label }, style = MaterialTheme.typography.bodyMedium)
},
supportingContent = {
if (!node.online)
@@ -120,7 +119,7 @@ fun MullvadItem(nav: ExitNodePickerNav, count: Int, selected: Boolean) {
headlineContent = {
Text(
stringResource(R.string.mullvad_exit_nodes),
- style = MaterialTheme.typography.titleMedium)
+ style = MaterialTheme.typography.bodyMedium)
},
supportingContent = {
Text(
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 b7a78e6..0f31529 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
@@ -15,6 +15,8 @@ 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.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
@@ -43,6 +45,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -59,6 +63,7 @@ import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.Permission
import com.tailscale.ipn.ui.model.Permissions
import com.tailscale.ipn.ui.model.Tailcfg
+import com.tailscale.ipn.ui.theme.disabled
import com.tailscale.ipn.ui.theme.listItem
import com.tailscale.ipn.ui.theme.primaryListItem
import com.tailscale.ipn.ui.theme.secondaryButton
@@ -252,7 +257,7 @@ fun ConnectView(
painter = painterResource(id = R.drawable.power),
contentDescription = null,
modifier = Modifier.size(40.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ tint = MaterialTheme.colorScheme.disabled)
Text(
text = stringResource(id = R.string.not_connected),
fontSize = MaterialTheme.typography.titleMedium.fontSize,
@@ -261,7 +266,13 @@ fun ConnectView(
fontFamily = MaterialTheme.typography.titleMedium.fontFamily)
val tailnetName = user.NetworkProfile?.DomainName ?: ""
Text(
- stringResource(id = R.string.connect_to_tailnet, tailnetName),
+ buildAnnotatedString {
+ append(stringResource(id = R.string.connect_to_tailnet_prefix))
+ pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
+ append(tailnetName)
+ pop()
+ append(stringResource(id = R.string.connect_to_tailnet_suffix))
+ },
fontSize = MaterialTheme.typography.titleMedium.fontSize,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
@@ -327,9 +338,16 @@ fun PeerList(
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
+ var first = true
peerList.value.forEach { peerSet ->
+ if (!first) {
+ item(key = "spacer_${peerSet.user?.DisplayName}") { Spacer(Modifier.height(24.dp)) }
+ }
+ first = false
+
stickyHeader {
ListItem(
+ modifier = Modifier.heightIn(max = 48.dp),
headlineContent = {
Text(
text =
@@ -338,6 +356,7 @@ fun PeerList(
fontWeight = FontWeight.SemiBold)
})
}
+
itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer ->
ListItem(
modifier = Modifier.clickable { onNavigateToPeerDetails(peer) },
diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePickerList.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePickerList.kt
index 63527bb..20e7f5c 100644
--- a/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePickerList.kt
+++ b/android/src/main/java/com/tailscale/ipn/ui/view/MullvadExitNodePickerList.kt
@@ -70,7 +70,7 @@ fun MullvadExitNodePickerList(
)
},
headlineContent = {
- Text(first.country, style = MaterialTheme.typography.titleMedium)
+ Text(first.country, style = MaterialTheme.typography.bodyMedium)
},
supportingContent = {
Text(
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index 4c7d716..d371083 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -46,7 +46,8 @@
EXIT NODE
Starting…
- "Connect again to talk to the other devices in the %1$s tailnet."
+ "Connect again to talk to the other devices in the "
+ " tailnet."
Welcome to Tailscale
Log in to join your tailnet and connect your devices.