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.