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 <andrea@gottardo.me>
pull/203/head
Andrea Gottardo 2 months ago
parent 4df18951a6
commit 4f46ce15a9

@ -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)

@ -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
)
}

@ -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
)
}

@ -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<Int>, 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<String>,
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<String>,
// 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<String>,
}
}
}
}
}

@ -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()
})
}
}

@ -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)
})
}
}
}
}

@ -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
}
}
}

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M13,3h-2v10h2L13,3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.74 -1.23,-5.18 -3.17,-6.83z"/>
</vector>

@ -37,10 +37,12 @@
<string name="bug_report">Bug Report</string>
<string name="use_ts_dns">Use Tailscale DNS</string>
<!-- Strings for the main scren -->
<!-- Strings for the main screen -->
<string name="exit_node">Exit Node</string>
<string name="starting">Starting…</string>
<string name="connect_to_tailnet">"Connect to your %1$s tailnet"</string>
<string name="connect_to_tailnet">"Connect again to talk to the other devices in the %1$s tailnet."</string>
<string name="welcome_to_tailscale">Welcome to Tailscale</string>
<string name="login_to_join_your_tailnet">Log in to join your tailnet and connect your devices.</string>
<!-- Strings for peer details -->
<string name="addresses_section">TAILSCALE ADDRESSES</string>

Loading…
Cancel
Save