From 4f46ce15a9e369e552a00a27ed5e215dd1c1107e Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Wed, 13 Mar 2024 18:03:59 -0700 Subject: [PATCH] ui: assorted UI tweaks + disconnected view Updates tailscale/corp#18202 - Begins using the brand theme colors for both light and dark mode - Adds a symbol to match the disconnected iOS view - Adds a PrimaryActionButton for blue colored buttons - Adds a static TailscaleLogoView, progress indicator mode to follow Signed-off-by: Andrea Gottardo --- .../java/com/tailscale/ipn/ui/theme/Color.kt | 75 ++--------- .../java/com/tailscale/ipn/ui/theme/Theme.kt | 79 +++-------- .../java/com/tailscale/ipn/ui/util/Buttons.kt | 36 +++++ .../com/tailscale/ipn/ui/view/MainView.kt | 127 +++++++++++++----- .../com/tailscale/ipn/ui/view/SettingsView.kt | 53 +++++--- .../ipn/ui/view/TailscaleLogoView.kt | 63 +++++++++ .../ipn/ui/viewModel/PeerDetailsViewModel.kt | 5 +- android/src/main/res/drawable/power.xml | 5 + android/src/main/res/values/strings.xml | 6 +- 9 files changed, 264 insertions(+), 185 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/ui/util/Buttons.kt create mode 100644 android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt create mode 100644 android/src/main/res/drawable/power.xml diff --git a/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt b/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt index d8bae61..6f240a0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt @@ -6,67 +6,16 @@ package com.tailscale.ipn.ui.theme import androidx.compose.ui.graphics.Color -val md_theme_light_primary = Color(0xFF006A61) -val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFF73F8E8) -val md_theme_light_onPrimaryContainer = Color(0xFF00201D) -val md_theme_light_secondary = Color(0xFF4A635F) -val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFCCE8E3) -val md_theme_light_onSecondaryContainer = Color(0xFF05201C) -val md_theme_light_tertiary = Color(0xFF46617A) -val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFCDE5FF) -val md_theme_light_onTertiaryContainer = Color(0xFF001D32) -val md_theme_light_error = Color(0xFFBA1A1A) -val md_theme_light_errorContainer = Color(0xFFFFDAD6) -val md_theme_light_onError = Color(0xFFFFFFFF) -val md_theme_light_onErrorContainer = Color(0xFF410002) -val md_theme_light_background = Color(0xFFFAFDFB) -val md_theme_light_onBackground = Color(0xFF191C1C) -val md_theme_light_surface = Color(0xFFFAFDFB) -val md_theme_light_onSurface = Color(0xFF191C1C) -val md_theme_light_surfaceVariant = Color(0xFFDAE5E2) -val md_theme_light_onSurfaceVariant = Color(0xFF3F4947) -val md_theme_light_outline = Color(0xFF6F7977) -val md_theme_light_inverseOnSurface = Color(0xFFEFF1EF) -val md_theme_light_inverseSurface = Color(0xFF2D3130) -val md_theme_light_inversePrimary = Color(0xFF52DBCB) -val md_theme_light_shadow = Color(0xFF000000) -val md_theme_light_surfaceTint = Color(0xFF006A61) -val md_theme_light_outlineVariant = Color(0xFFBEC9C6) -val md_theme_light_scrim = Color(0xFF000000) +val ts_color_light_primary = Color(0xFF232222) +val ts_color_light_secondary = Color(0xFF706E6D) +val ts_color_light_background = Color(0xFFFFFFFF) +val ts_color_light_tintedBackground = Color(0xFFF7F5F4) +val ts_color_light_blue = Color(0xFF4B70CC) +val ts_color_light_green = Color(0xFF1EA672) -val md_theme_dark_primary = Color(0xFF52DBCB) -val md_theme_dark_onPrimary = Color(0xFF003732) -val md_theme_dark_primaryContainer = Color(0xFF005049) -val md_theme_dark_onPrimaryContainer = Color(0xFF73F8E8) -val md_theme_dark_secondary = Color(0xFFB1CCC7) -val md_theme_dark_onSecondary = Color(0xFF1C3531) -val md_theme_dark_secondaryContainer = Color(0xFF324B48) -val md_theme_dark_onSecondaryContainer = Color(0xFFCCE8E3) -val md_theme_dark_tertiary = Color(0xFFAEC9E6) -val md_theme_dark_onTertiary = Color(0xFF163349) -val md_theme_dark_tertiaryContainer = Color(0xFF2E4961) -val md_theme_dark_onTertiaryContainer = Color(0xFFCDE5FF) -val md_theme_dark_error = Color(0xFFFFB4AB) -val md_theme_dark_errorContainer = Color(0xFF93000A) -val md_theme_dark_onError = Color(0xFF690005) -val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -val md_theme_dark_background = Color(0xFF191C1C) -val md_theme_dark_onBackground = Color(0xFFE0E3E1) -val md_theme_dark_surface = Color(0xFF191C1C) -val md_theme_dark_onSurface = Color(0xFFE0E3E1) -val md_theme_dark_surfaceVariant = Color(0xFF3F4947) -val md_theme_dark_onSurfaceVariant = Color(0xFFBEC9C6) -val md_theme_dark_outline = Color(0xFF899390) -val md_theme_dark_inverseOnSurface = Color(0xFF191C1C) -val md_theme_dark_inverseSurface = Color(0xFFE0E3E1) -val md_theme_dark_inversePrimary = Color(0xFF006A61) -val md_theme_dark_shadow = Color(0xFF000000) -val md_theme_dark_surfaceTint = Color(0xFF52DBCB) -val md_theme_dark_outlineVariant = Color(0xFF3F4947) -val md_theme_dark_scrim = Color(0xFF000000) - - -val seed = Color(0xFF006B62) +val ts_color_dark_primary = Color(0xFFFAF9F8) +val ts_color_dark_secondary = Color(0xFFAFACAB) +val ts_color_dark_background = Color(0xFF232222) +val ts_color_dark_tintedBackground = Color(0xFF2E2D2D) +val ts_color_dark_blue = Color(0xFF4B70CC) +var ts_color_dark_green = Color(0xFF33C27F) \ No newline at end of file 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 8ae74d1..d68a16e 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 @@ -12,74 +12,27 @@ import androidx.compose.runtime.Composable private val LightColors = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim, + primary = ts_color_light_primary, + onPrimary = ts_color_light_background, + secondary = ts_color_light_secondary, + onSecondary = ts_color_light_background, + secondaryContainer = ts_color_light_tintedBackground, + surface = ts_color_light_background, ) - private val DarkColors = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim, + primary = ts_color_dark_primary, + onPrimary = ts_color_dark_background, + secondary = ts_color_dark_secondary, + onSecondary = ts_color_dark_background, + secondaryContainer = ts_color_dark_tintedBackground, + surface = ts_color_dark_background, ) @Composable fun AppTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable() () -> Unit + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit ) { val colors = if (!useDarkTheme) { LightColors @@ -88,7 +41,7 @@ fun AppTheme( } MaterialTheme( - colorScheme = colors, - content = content + colorScheme = colors, + content = content ) } \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/Buttons.kt b/android/src/main/java/com/tailscale/ipn/ui/util/Buttons.kt new file mode 100644 index 0000000..95bb574 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/util/Buttons.kt @@ -0,0 +1,36 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.util + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tailscale.ipn.ui.theme.ts_color_light_blue + +@Composable +fun PrimaryActionButton( + onClick: () -> Unit, + content: @Composable RowScope.() -> Unit +) { + Button( + onClick = onClick, + colors = ButtonColors( + containerColor = ts_color_light_blue, + contentColor = Color.White, + disabledContainerColor = MaterialTheme.colorScheme.secondary, + disabledContentColor = MaterialTheme.colorScheme.onSecondary + ), + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier + .fillMaxWidth(), + content = content + ) +} \ No newline at end of file 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 4cedb91..a959778 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 @@ -11,6 +11,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -22,7 +23,6 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -40,14 +40,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +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.unit.dp import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.Tailcfg +import com.tailscale.ipn.ui.theme.ts_color_light_green import com.tailscale.ipn.ui.util.PeerSet +import com.tailscale.ipn.ui.util.PrimaryActionButton import com.tailscale.ipn.ui.viewModel.MainViewModel import kotlinx.coroutines.flow.StateFlow @@ -61,7 +66,7 @@ data class MainViewNavigation( @Composable fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) { - Surface(color = MaterialTheme.colorScheme.background) { + Surface(color = MaterialTheme.colorScheme.secondaryContainer) { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Center @@ -76,6 +81,7 @@ fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) { val isOn = viewModel.vpnToggleState.collectAsState(initial = false) Switch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value) + Spacer(Modifier.size(3.dp)) StateDisplay(viewModel.stateRes, viewModel.userName) Box(modifier = Modifier .weight(1f) @@ -136,9 +142,9 @@ fun StateDisplay(state: StateFlow, tailnet: String) { val stateVal = state.collectAsState(initial = R.string.placeholder) val stateStr = stringResource(id = stateVal.value) - Column(modifier = Modifier.padding(6.dp)) { + Column(modifier = Modifier.padding(7.dp)) { Text(text = tailnet, style = MaterialTheme.typography.titleMedium) - Text(text = stateStr, style = MaterialTheme.typography.bodyMedium) + Text(text = stateStr, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary) } } @@ -176,26 +182,72 @@ fun StartingView() { @Composable fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit) { - Column( - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = stringResource(id = R.string.not_connected), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary) - if (user != null && !user.isEmpty()) { - val tailnetName = user.NetworkProfile?.DomainName ?: "" - Text(stringResource(id = R.string.connect_to_tailnet, tailnetName), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - Button(onClick = connectAction) { Text(text = stringResource(id = R.string.connect)) } - } else { - Button(onClick = loginAction) { Text(text = stringResource(id = R.string.log_in)) } + Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.secondaryContainer) + .padding(8.dp) + .fillMaxWidth(0.7f) + .fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy( + 8.dp, + alignment = Alignment.CenterVertically + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (user != null && !user.isEmpty()) { + Icon( + painter = painterResource(id = R.drawable.power), + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(id = R.string.not_connected), + fontSize = MaterialTheme.typography.titleMedium.fontSize, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + fontFamily = MaterialTheme.typography.titleMedium.fontFamily + ) + val tailnetName = user.NetworkProfile?.DomainName ?: "" + Text( + stringResource(id = R.string.connect_to_tailnet, tailnetName), + fontSize = MaterialTheme.typography.titleMedium.fontSize, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.size(1.dp)) + PrimaryActionButton(onClick = connectAction) { + Text( + text = stringResource(id = R.string.connect), + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) + } + } else { + TailscaleLogoView(Modifier.size(50.dp)) + Spacer(modifier = Modifier.size(1.dp)) + Text( + text = stringResource(id = R.string.welcome_to_tailscale), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center + ) + Text( + stringResource(R.string.login_to_join_your_tailnet), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.size(1.dp)) + PrimaryActionButton(onClick = loginAction) { + Text( + text = stringResource(id = R.string.log_in), + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) + } + } } } } @@ -214,17 +266,18 @@ fun PeerList(searchTerm: StateFlow, val stateVal = state.collectAsState(initial = Ipn.State.NoState) SearchBar( - query = searchTermStr, - onQueryChange = onSearch, - onSearch = onSearch, - active = true, - onActiveChange = { searching = it }, - shape = RoundedCornerShape(10.dp), - leadingIcon = { Icon(Icons.Outlined.Search, null) }, - tonalElevation = 2.dp, - shadowElevation = 2.dp, - colors = SearchBarDefaults.colors(), - modifier = Modifier.fillMaxWidth()) { + query = searchTermStr, + onQueryChange = onSearch, + onSearch = onSearch, + active = true, + onActiveChange = { searching = it }, + shape = RoundedCornerShape(10.dp), + leadingIcon = { Icon(Icons.Outlined.Search, null) }, + tonalElevation = 2.dp, + shadowElevation = 2.dp, + colors = SearchBarDefaults.colors(), + modifier = Modifier.fillMaxWidth() + ) { LazyColumn( modifier = @@ -250,7 +303,7 @@ fun PeerList(searchTerm: StateFlow, // By definition, SelfPeer is online since we will not show the peer list unless you're connected. val isSelfAndRunning = (peer.StableID == selfPeer && stateVal.value == Ipn.State.Running) val color: Color = if ((peer.Online == true) || isSelfAndRunning) { - Color.Green + ts_color_light_green } else { Color.Gray } @@ -277,4 +330,4 @@ fun PeerList(searchTerm: StateFlow, } } } -} \ No newline at end of file +} 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 4b64ff3..488a6ad 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 @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links import com.tailscale.ipn.ui.model.IpnLocal +import com.tailscale.ipn.ui.util.PrimaryActionButton import com.tailscale.ipn.ui.util.defaultPaddingModifier import com.tailscale.ipn.ui.util.settingsRowModifier import com.tailscale.ipn.ui.viewModel.Setting @@ -47,25 +48,27 @@ import com.tailscale.ipn.ui.viewModel.SettingsViewModel data class SettingsNav( - val onNavigateToBugReport: () -> Unit, - val onNavigateToAbout: () -> Unit, - val onNavigateToMDMSettings: () -> Unit, - val onNavigateToManagedBy: () -> Unit, + val onNavigateToBugReport: () -> Unit, + val onNavigateToAbout: () -> Unit, + val onNavigateToMDMSettings: () -> Unit, + val onNavigateToManagedBy: () -> Unit, ) @Composable fun Settings(viewModel: SettingsViewModel) { val handler = LocalUriHandler.current - Surface(color = MaterialTheme.colorScheme.surface) { + Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxHeight()) { Column(modifier = defaultPaddingModifier().fillMaxHeight()) { - Text(text = stringResource(id = R.string.settings_title), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.titleMedium) + Text( + text = stringResource(id = R.string.settings_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium + ) Spacer(modifier = Modifier.height(8.dp)) @@ -79,7 +82,7 @@ fun Settings(viewModel: SettingsViewModel) { handler.openUri(Links.ADMIN_URL) }) Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { viewModel.ipnManager.logout() }) { + PrimaryActionButton(onClick = { viewModel.ipnManager.logout() }) { Text(text = stringResource(id = R.string.log_out)) } } ?: run { @@ -93,7 +96,11 @@ fun Settings(viewModel: SettingsViewModel) { viewModel.settings.forEach { settingBundle -> Column(modifier = settingsRowModifier()) { settingBundle.title?.let { - Text(text = it, style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(8.dp) + ) } settingBundle.settings.forEach { setting -> when (setting.type) { @@ -118,7 +125,12 @@ fun Settings(viewModel: SettingsViewModel) { } @Composable -fun UserView(profile: IpnLocal.LoginProfile?, isAdmin: Boolean, adminText: AnnotatedString, onClick: () -> Unit) { +fun UserView( + profile: IpnLocal.LoginProfile?, + isAdmin: Boolean, + adminText: AnnotatedString, + onClick: () -> Unit +) { Column { Row(modifier = settingsRowModifier().padding(8.dp)) { @@ -127,17 +139,22 @@ fun UserView(profile: IpnLocal.LoginProfile?, isAdmin: Boolean, adminText: Annot } Column(verticalArrangement = Arrangement.Center) { - Text(text = profile?.UserProfile?.DisplayName - ?: "", style = MaterialTheme.typography.titleMedium) + Text( + text = profile?.UserProfile?.DisplayName + ?: "", style = MaterialTheme.typography.titleMedium + ) Text(text = profile?.Name ?: "", style = MaterialTheme.typography.bodyMedium) } } if (isAdmin) { Column(modifier = Modifier.padding(horizontal = 12.dp)) { - ClickableText(text = adminText, style = MaterialTheme.typography.bodySmall, onClick = { - onClick() - }) + ClickableText( + text = adminText, + style = MaterialTheme.typography.bodySmall, + onClick = { + onClick() + }) } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt new file mode 100644 index 0000000..88e3f68 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt @@ -0,0 +1,63 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.view + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +// TODO(angott): +// - Implement game-of-life animation for progress indicator. +// - Remove hardcoded dots, use a for-each and make it dynamically +// use the space available instead of unit = 10.dp + +@Composable +fun TailscaleLogoView(modifier: Modifier) { + val primaryColor: Color = MaterialTheme.colorScheme.primary + val secondaryColor: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) + BoxWithConstraints(modifier) { + Column(verticalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) { + Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) { + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = secondaryColor) + }) + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = secondaryColor) + }) + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = secondaryColor) + }) + } + Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) { + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = primaryColor) + }) + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = primaryColor) + }) + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = primaryColor) + }) + } + Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) { + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = secondaryColor) + }) + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = primaryColor) + }) + Canvas(modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)), onDraw = { + drawCircle(color = secondaryColor) + }) + } + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt index 2651dcf..e58167f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.service.IpnModel +import com.tailscale.ipn.ui.theme.ts_color_light_green import com.tailscale.ipn.ui.util.ComposableStringFormatter import com.tailscale.ipn.ui.util.DisplayAddress import com.tailscale.ipn.ui.util.TimeUtil @@ -47,6 +48,6 @@ class PeerDetailsViewModel(val model: IpnModel, val nodeId: StableNodeID) : View nodeName = peer?.ComputedName ?: "" connectedStrRes = if (peer?.Online == true) R.string.connected else R.string.not_connected - connectedColor = if (peer?.Online == true) Color.Green else Color.Gray + connectedColor = if (peer?.Online == true) ts_color_light_green else Color.Gray } -} \ No newline at end of file +} diff --git a/android/src/main/res/drawable/power.xml b/android/src/main/res/drawable/power.xml new file mode 100644 index 0000000..6cc717e --- /dev/null +++ b/android/src/main/res/drawable/power.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 17e67a5..5ab3f94 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -37,10 +37,12 @@ Bug Report Use Tailscale DNS - + Exit Node Starting… - "Connect to your %1$s tailnet" + "Connect again to talk to the other devices in the %1$s tailnet." + Welcome to Tailscale + Log in to join your tailnet and connect your devices. TAILSCALE ADDRESSES