From 89b3a04c0ad1c285167faba5f6012416fc443f92 Mon Sep 17 00:00:00 2001 From: Percy Wegmann Date: Fri, 29 Mar 2024 10:36:09 -0500 Subject: [PATCH] More styling progress Signed-off-by: Percy Wegmann --- .../java/com/tailscale/ipn/ui/theme/Theme.kt | 62 +++++++++++- .../ipn/ui/util/ClipboardValueView.kt | 4 +- .../java/com/tailscale/ipn/ui/util/Lists.kt | 2 +- .../com/tailscale/ipn/ui/view/MainView.kt | 94 ++++++++++--------- .../com/tailscale/ipn/ui/view/SettingsView.kt | 4 + .../com/tailscale/ipn/ui/view/SharedViews.kt | 3 + 6 files changed, 119 insertions(+), 50 deletions(-) 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 5fe0471..1a5949f 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 @@ -5,7 +5,12 @@ package com.tailscale.ipn.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItemColors +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme @@ -24,8 +29,11 @@ fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable val typography = Typography( + // titleMedium is styled to be slightly larger than bodyMedium for emphasis titleMedium = 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)) @@ -43,8 +51,10 @@ private val LightColors = errorContainer = Color(0xFFFEF6F3), // red-0 onErrorContainer = Color(0xFF930921), // red-600 surfaceDim = Color(0xFFF7F5F4), // gray-100 - surface = Color(0xFFF7F5F4), // gray-100 - background = Color(0xFFF7F5F4), // gray-100 + // surface = Color(0xFFF7F5F4), // gray-100 + surface = Color(0xFFFFFFFF), // white, + // background = Color(0xFFF7F5F4), // gray-100 + background = Color(0xFFFFFFFF), // white surfaceBright = Color(0xFFFFFFFF), // white surfaceContainerLowest = Color(0xFFFFFFFF), // white surfaceContainerLow = Color(0xFFF7F5F4), // gray-100 @@ -93,3 +103,51 @@ val ColorScheme.successContainer: Color val ColorScheme.onSuccessContainer: Color get() = Color(0xFF0E4B3B) // green-600 + +/** + * Main color scheme for list items, uses onPrimaryContainer color for leading and trailing icons. + */ +val ColorScheme.listItem: ListItemColors + @Composable + get() { + val default = ListItemDefaults.colors() + return ListItemColors( + containerColor = default.containerColor, + headlineColor = default.headlineColor, + leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + overlineColor = default.overlineColor, + supportingTextColor = default.supportingTextColor, + trailingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + disabledHeadlineColor = default.disabledHeadlineColor, + disabledLeadingIconColor = default.disabledLeadingIconColor, + disabledTrailingIconColor = default.disabledTrailingIconColor) + } + +/** Color scheme for list items that should be styled as a surface container. */ +val ColorScheme.containerListItem: ListItemColors + @Composable + get() { + val default = ListItemDefaults.colors() + return ListItemColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + headlineColor = MaterialTheme.colorScheme.onSurface, + leadingIconColor = MaterialTheme.colorScheme.onSurface, + overlineColor = MaterialTheme.colorScheme.onSurface, + supportingTextColor = MaterialTheme.colorScheme.onSurface, + trailingIconColor = MaterialTheme.colorScheme.onSurface, + disabledHeadlineColor = default.disabledHeadlineColor, + disabledLeadingIconColor = default.disabledLeadingIconColor, + disabledTrailingIconColor = default.disabledTrailingIconColor) + } + +/** Main color scheme for top app bar, styles it as a surface container. */ +@OptIn(ExperimentalMaterial3Api::class) +val ColorScheme.topAppBar: TopAppBarColors + @Composable + get() = + TopAppBarDefaults.topAppBarColors() + .copy( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onSurface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + ) 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 20c0d8c..2ab957f 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 @@ -22,7 +22,6 @@ 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.ts_color_light_blue @Composable fun ClipboardValueView( @@ -56,8 +55,7 @@ fun ClipboardValueView( Icon( painterResource(R.drawable.clipboard), stringResource(R.string.copy_to_clipboard), - modifier = Modifier.width(24.dp).height(24.dp), - tint = ts_color_light_blue) + modifier = Modifier.width(24.dp).height(24.dp)) }) } } 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 f834a1c..256b19d 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 @@ -26,7 +26,7 @@ object Lists { @Composable fun ItemDivider() { - HorizontalDivider(color = MaterialTheme.colorScheme.outline) + HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainer) } } 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 e401d12..0eb7e9e 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 @@ -44,6 +44,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -56,6 +57,9 @@ 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.containerListItem +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.PeerSet import com.tailscale.ipn.ui.util.flag @@ -84,11 +88,13 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode val username = viewModel.userName ListItem( + colors = MaterialTheme.colorScheme.containerListItem, leadingContent = { val isOn = viewModel.vpnToggleState.collectAsState(initial = false) - if (state.value != Ipn.State.NoState) { - TintedSwitch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value) - } + TintedSwitch( + onCheckedChange = { viewModel.toggleVpn() }, + checked = isOn.value, + enabled = state.value != Ipn.State.NoState) }, headlineContent = { if (username.isNotEmpty()) { @@ -119,10 +125,8 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode PromptPermissionsIfNecessary(permissions = Permissions.all) - Row(modifier = Modifier.padding(top = 10.dp, bottom = 20.dp)) { - ExitNodeStatus( - navAction = navigation.onNavigateToExitNodes, viewModel = viewModel) - } + ExitNodeStatus(navAction = navigation.onNavigateToExitNodes, viewModel = viewModel) + PeerList( viewModel = viewModel, onNavigateToPeerDetails = navigation.onNavigateToPeerDetails, @@ -146,44 +150,47 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) { val exitNodeId = prefs.value?.ExitNodeID val peer = exitNodeId?.let { id -> netmap.value?.Peers?.find { it.StableID == id } } val location = peer?.Hostinfo?.Location - val name = peer?.Name + val name = peer?.ComputedName - Box( - modifier = - Modifier.padding(horizontal = 16.dp) - .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) - .background(MaterialTheme.colorScheme.background) - .fillMaxWidth()) { - ListItem( - modifier = Modifier.clickable { navAction() }, - headlineContent = { - Text( - stringResource(R.string.exit_node), - style = MaterialTheme.typography.titleMedium, - ) - }, - supportingContent = { - Row(verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainer)) { + Box( + modifier = + Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 16.dp) + .clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp)) + .fillMaxWidth()) { + ListItem( + modifier = Modifier.clickable { navAction() }, + overlineContent = { Text( - text = - location?.let { "${it.CountryCode?.flag()} ${it.Country} - ${it.City}" } - ?: name - ?: stringResource(id = R.string.none), - style = MaterialTheme.typography.bodyLarge) - Icon( - Icons.Outlined.ArrowDropDown, - null, + stringResource(R.string.exit_node), + style = MaterialTheme.typography.bodyMedium, ) - } - }, - trailingContent = { - if (peer != null) { - Button(onClick = { viewModel.disableExitNode() }) { - Text(stringResource(R.string.disable)) + }, + headlineContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = + location?.let { "${it.CountryCode?.flag()} ${it.Country} - ${it.City}" } + ?: name + ?: stringResource(id = R.string.none), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis) + Icon( + Icons.Outlined.ArrowDropDown, + null, + ) } - } - }) - } + }, + trailingContent = { + if (peer != null) { + Button(onClick = { viewModel.disableExitNode() }) { + Text(stringResource(R.string.disable)) + } + } + }) + } + } } @Composable @@ -278,7 +285,6 @@ fun PeerList( ) { val peerList = viewModel.peers.collectAsState(initial = emptyList()) val searchTermStr by viewModel.searchTerm.collectAsState(initial = "") - val stateVal = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState) val netmap = viewModel.netmap.collectAsState() SearchBar( @@ -298,6 +304,7 @@ fun PeerList( SearchBarDefaults.colors( containerColor = Color.Transparent, dividerColor = Color.Transparent), modifier = Modifier.fillMaxWidth()) { + Lists.ItemDivider() LazyColumn( modifier = Modifier.fillMaxSize(), ) { @@ -315,10 +322,9 @@ fun PeerList( itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer -> ListItem( modifier = Modifier.clickable { onNavigateToPeerDetails(peer) }, + colors = MaterialTheme.colorScheme.listItem, headlineContent = { Row(verticalAlignment = Alignment.CenterVertically) { - // By definition, SelfPeer is online since we will not show the peer list - // unless you're connected. Box( modifier = Modifier.size(10.dp) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index b126d47..ccbfe74 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tailscale.ipn.BuildConfig import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links +import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.viewModel.Setting import com.tailscale.ipn.ui.viewModel.SettingType @@ -104,6 +105,7 @@ private fun TextRow(setting: Setting) { val enabled = setting.enabled.collectAsState().value ListItem( modifier = Modifier.clickable { if (enabled) setting.onClick() }, + colors = MaterialTheme.colorScheme.listItem, headlineContent = { Text( setting.title ?: stringResource(setting.titleRes), @@ -119,6 +121,7 @@ private fun SwitchRow(setting: Setting) { val swVal = setting.isOn?.collectAsState()?.value ?: false ListItem( modifier = Modifier.clickable { if (enabled) setting.onClick() }, + colors = MaterialTheme.colorScheme.listItem, headlineContent = { Text( setting.title ?: stringResource(setting.titleRes), @@ -134,6 +137,7 @@ private fun SwitchRow(setting: Setting) { private fun NavRow(setting: Setting) { ListItem( modifier = Modifier.clickable { setting.onClick() }, + colors = MaterialTheme.colorScheme.listItem, headlineContent = { Text( setting.title ?: stringResource(setting.titleRes), diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt index 7604faa..7d2553d 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt @@ -14,12 +14,14 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.tailscale.ipn.ui.theme.topAppBar import com.tailscale.ipn.ui.theme.ts_color_light_blue data class BackNavigation( @@ -32,6 +34,7 @@ data class BackNavigation( fun Header(@StringRes title: Int = 0, titleText: String? = null, onBack: (() -> Unit)? = null) { TopAppBar( title = { Text(titleText ?: stringResource(title)) }, + colors = MaterialTheme.colorScheme.topAppBar, navigationIcon = { onBack?.let { BackArrow(action = it) } }, ) }