diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt index f8d82aa..beab06b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt @@ -105,11 +105,13 @@ class Tailcfg { fun connectedColor(nm: Netmap.NetworkMap?) = if (connectedOrSelfNode(nm)) MaterialTheme.colorScheme.on else MaterialTheme.colorScheme.off + val nameWithoutTrailingDot = Name.trimEnd('.') + val displayAddresses: List get() { var addresses = mutableListOf() + addresses.add(DisplayAddress(NameWithoutTrailingDot)) Addresses?.let { addresses.addAll(it.map { addr -> DisplayAddress(addr) }) } - addresses.add(DisplayAddress(Name)) return addresses } 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 6957b6f..9f8daa0 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 @@ -44,7 +44,8 @@ fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable val systemUiController = rememberSystemUiController() DisposableEffect(systemUiController, useDarkTheme) { - systemUiController.setSystemBarsColor(color = colors.surfaceContainer) + systemUiController.setStatusBarColor(color = colors.surfaceContainer) + systemUiController.setNavigationBarColor(color = Color.Black) onDispose {} } @@ -117,7 +118,7 @@ val ColorScheme.onSuccessContainer: Color get() = Color(0xFF0E4B3B) // green-600 val ColorScheme.on: Color - get() = Color(0xFF84D996) // green-100 + get() = Color(0xFF1CA672) // green-300 val ColorScheme.off: Color get() = Color(0xFFD9D6D5) // gray-300 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 9b5502c..4c76a4b 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 @@ -35,7 +35,6 @@ inline fun LazyListScope.itemsWithDividers( items: List, noinline key: ((item: T) -> Any)? = null, forceLeading: Boolean = false, - forceTrailing: Boolean = false, crossinline contentType: (item: T) -> Any? = { _ -> null }, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit ) = @@ -43,9 +42,7 @@ inline fun LazyListScope.itemsWithDividers( count = items.size, key = if (key != null) { index: Int -> key(items[index]) } else null, contentType = { index -> contentType(items[index]) }) { - if (forceLeading && it == 0 || - forceTrailing && it == items.size || - it > 0 && it < items.size) { + if (forceLeading && it == 0 || it > 0 && it < items.size) { Lists.ItemDivider() } itemContent(items[it]) @@ -55,7 +52,6 @@ inline fun LazyListScope.itemsWithDividers( items: Array, noinline key: ((item: T) -> Any)? = null, forceLeading: Boolean = false, - forceTrailing: Boolean = false, crossinline contentType: (item: T) -> Any? = { _ -> null }, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit -) = itemsWithDividers(items.toList(), key, forceLeading, forceTrailing, contentType, itemContent) +) = itemsWithDividers(items.toList(), key, forceLeading, contentType, itemContent) diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt b/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt index e4c92f6..e65568d 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt @@ -45,7 +45,7 @@ class PeerCategorizer { if (it.user?.ID == me?.ID) { "" } else { - it.user?.DisplayName ?: "Unknown User" + it.user?.DisplayName?.lowercase() ?: "unknown user" } } } 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 0f31529..f801667 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 @@ -68,6 +68,7 @@ import com.tailscale.ipn.ui.theme.listItem import com.tailscale.ipn.ui.theme.primaryListItem import com.tailscale.ipn.ui.theme.secondaryButton import com.tailscale.ipn.ui.theme.surfaceContainerListItem +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 @@ -341,7 +342,10 @@ fun PeerList( var first = true peerList.value.forEach { peerSet -> if (!first) { - item(key = "spacer_${peerSet.user?.DisplayName}") { Spacer(Modifier.height(24.dp)) } + item(key = "spacer_${peerSet.user?.DisplayName}") { + Lists.ItemDivider() + Spacer(Modifier.height(24.dp)) + } } first = false 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 bb76710..4903890 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 @@ -6,35 +6,44 @@ package com.tailscale.ipn.ui.view import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import com.tailscale.ipn.R +import com.tailscale.ipn.ui.theme.listItem +import com.tailscale.ipn.ui.theme.surfaceContainerListItem +import com.tailscale.ipn.ui.theme.topAppBar import com.tailscale.ipn.ui.theme.ts_color_light_blue -import com.tailscale.ipn.ui.util.settingsRowModifier +import com.tailscale.ipn.ui.util.Lists +import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PeerDetails( nav: BackNavigation, @@ -42,51 +51,68 @@ fun PeerDetails( model: PeerDetailsViewModel = viewModel(factory = PeerDetailsViewModelFactory(nodeId, LocalContext.current.filesDir)) ) { - Scaffold(topBar = { Header(title = R.string.peer_details, onBack = nav.onBack) }) { innerPadding - -> - Column( - modifier = - Modifier.fillMaxWidth() - .padding(innerPadding) - .padding(horizontal = 16.dp) - .padding(top = 22.dp), - ) { - model.netmap.collectAsState().value?.let { netmap -> - model.node.collectAsState().value?.let { node -> - Text(text = node.displayName, style = MaterialTheme.typography.titleLarge) - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = - Modifier.size(8.dp) - .background( - color = node.connectedColor(netmap), - shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(id = node.connectedStrRes(netmap)), - style = MaterialTheme.typography.bodyMedium) - } - Column(modifier = Modifier.fillMaxHeight()) { - Text( - text = stringResource(id = R.string.addresses_section), - style = MaterialTheme.typography.titleMedium) + model.netmap.collectAsState().value?.let { netmap -> + model.node.collectAsState().value?.let { node -> + Scaffold( + topBar = { + TopAppBar( + title = { + ListItem( + colors = MaterialTheme.colorScheme.surfaceContainerListItem, + headlineContent = { + Text( + text = node.displayName, + style = MaterialTheme.typography.titleMedium.copy(lineHeight = 20.sp)) + }, + supportingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.size(8.dp) + .background( + color = node.connectedColor(netmap), + shape = RoundedCornerShape(percent = 50))) {} + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(id = node.connectedStrRes(netmap)), + style = MaterialTheme.typography.bodyMedium.copy(lineHeight = 20.sp)) + } + }, + ) + }, + colors = MaterialTheme.colorScheme.topAppBar, + navigationIcon = { BackArrow(action = { nav.onBack() }) }, + ) + }) { innerPadding -> + LazyColumn( + modifier = Modifier.padding(innerPadding), + ) { + item(key = "tailscaleAddresses") { + Box( + modifier = + Modifier.fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surface, + shape = RectangleShape)) { + Text( + modifier = Modifier.padding(start = 16.dp, top = 16.dp), + text = stringResource(R.string.tailscale_addresses), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } - Column(modifier = settingsRowModifier()) { - node.displayAddresses.forEach { + itemsWithDividers(node.displayAddresses, key = { it.address }) { AddressRow(address = it.address, type = it.typeString) } - } - Spacer(modifier = Modifier.size(16.dp)) + item(key = "infoDivider") { Lists.SectionDivider() } - Column(modifier = settingsRowModifier()) { - node.info.forEach { + itemsWithDividers(node.info, key = { "info_${it.titleRes}" }) { ValueRow(title = stringResource(id = it.titleRes), value = it.value.getString()) } } } - } - } } } } @@ -95,30 +121,21 @@ fun PeerDetails( fun AddressRow(address: String, type: String) { val localClipboardManager = LocalClipboardManager.current - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(horizontal = 8.dp, vertical = 8.dp) - .clickable(onClick = { localClipboardManager.setText(AnnotatedString(address)) })) { - Column { - Text(text = address) - Text(text = type, fontSize = MaterialTheme.typography.labelLarge.fontSize) - } - Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Icon( - painter = painterResource(id = R.drawable.clipboard), - null, - tint = ts_color_light_blue) - } - } + ListItem( + modifier = Modifier.clickable { localClipboardManager.setText(AnnotatedString(address)) }, + colors = MaterialTheme.colorScheme.listItem, + headlineContent = { Text(text = address) }, + 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) + }) } @Composable fun ValueRow(title: String, value: String) { - Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp).fillMaxWidth()) { - Text(text = title) - Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Text(text = value) - } - } + ListItem( + colors = MaterialTheme.colorScheme.listItem, + headlineContent = { Text(text = title) }, + supportingContent = { Text(text = value) }) } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index ffb08ea..5780d45 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -52,10 +52,9 @@ Log in to join your tailnet and connect your devices. - OS - Key Expiry - Tailscale Addresses + Key expiry + Tailscale addresses Current MDM Settings