ui: assorted UI tweaks + disconnected view (#203)

pull/211/head
Andrea Gottardo 9 months ago committed by GitHub
parent 4df18951a6
commit 06e850bbd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,67 +6,16 @@ package com.tailscale.ipn.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF006A61) val ts_color_light_primary = Color(0xFF232222)
val md_theme_light_onPrimary = Color(0xFFFFFFFF) val ts_color_light_secondary = Color(0xFF706E6D)
val md_theme_light_primaryContainer = Color(0xFF73F8E8) val ts_color_light_background = Color(0xFFFFFFFF)
val md_theme_light_onPrimaryContainer = Color(0xFF00201D) val ts_color_light_tintedBackground = Color(0xFFF7F5F4)
val md_theme_light_secondary = Color(0xFF4A635F) val ts_color_light_blue = Color(0xFF4B70CC)
val md_theme_light_onSecondary = Color(0xFFFFFFFF) val ts_color_light_green = Color(0xFF1EA672)
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 md_theme_dark_primary = Color(0xFF52DBCB) val ts_color_dark_primary = Color(0xFFFAF9F8)
val md_theme_dark_onPrimary = Color(0xFF003732) val ts_color_dark_secondary = Color(0xFFAFACAB)
val md_theme_dark_primaryContainer = Color(0xFF005049) val ts_color_dark_background = Color(0xFF232222)
val md_theme_dark_onPrimaryContainer = Color(0xFF73F8E8) val ts_color_dark_tintedBackground = Color(0xFF2E2D2D)
val md_theme_dark_secondary = Color(0xFFB1CCC7) val ts_color_dark_blue = Color(0xFF4B70CC)
val md_theme_dark_onSecondary = Color(0xFF1C3531) var ts_color_dark_green = Color(0xFF33C27F)
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)

@ -12,68 +12,21 @@ import androidx.compose.runtime.Composable
private val LightColors = lightColorScheme( private val LightColors = lightColorScheme(
primary = md_theme_light_primary, primary = ts_color_light_primary,
onPrimary = md_theme_light_onPrimary, onPrimary = ts_color_light_background,
primaryContainer = md_theme_light_primaryContainer, secondary = ts_color_light_secondary,
onPrimaryContainer = md_theme_light_onPrimaryContainer, onSecondary = ts_color_light_background,
secondary = md_theme_light_secondary, secondaryContainer = ts_color_light_tintedBackground,
onSecondary = md_theme_light_onSecondary, surface = ts_color_light_background,
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,
) )
private val DarkColors = darkColorScheme( private val DarkColors = darkColorScheme(
primary = md_theme_dark_primary, primary = ts_color_dark_primary,
onPrimary = md_theme_dark_onPrimary, onPrimary = ts_color_dark_background,
primaryContainer = md_theme_dark_primaryContainer, secondary = ts_color_dark_secondary,
onPrimaryContainer = md_theme_dark_onPrimaryContainer, onSecondary = ts_color_dark_background,
secondary = md_theme_dark_secondary, secondaryContainer = ts_color_dark_tintedBackground,
onSecondary = md_theme_dark_onSecondary, surface = ts_color_dark_background,
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,
) )
@Composable @Composable

@ -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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.ArrowDropDown
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -40,14 +40,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.model.Tailcfg 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.PeerSet
import com.tailscale.ipn.ui.util.PrimaryActionButton
import com.tailscale.ipn.ui.viewModel.MainViewModel import com.tailscale.ipn.ui.viewModel.MainViewModel
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -61,7 +66,7 @@ data class MainViewNavigation(
@Composable @Composable
fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) { fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.secondaryContainer) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
@ -76,6 +81,7 @@ fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) {
val isOn = viewModel.vpnToggleState.collectAsState(initial = false) val isOn = viewModel.vpnToggleState.collectAsState(initial = false)
Switch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value) Switch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value)
Spacer(Modifier.size(3.dp))
StateDisplay(viewModel.stateRes, viewModel.userName) StateDisplay(viewModel.stateRes, viewModel.userName)
Box(modifier = Modifier Box(modifier = Modifier
.weight(1f) .weight(1f)
@ -136,9 +142,9 @@ fun StateDisplay(state: StateFlow<Int>, tailnet: String) {
val stateVal = state.collectAsState(initial = R.string.placeholder) val stateVal = state.collectAsState(initial = R.string.placeholder)
val stateStr = stringResource(id = stateVal.value) 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 = 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 @Composable
fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit) { fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = modifier = Modifier
Modifier .background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxSize() .padding(8.dp)
.background(MaterialTheme.colorScheme.background), .fillMaxWidth(0.7f)
verticalArrangement = Arrangement.Center, .fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally verticalArrangement = Arrangement.spacedBy(
8.dp,
alignment = Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text(text = stringResource(id = R.string.not_connected),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary)
if (user != null && !user.isEmpty()) { 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 ?: "" val tailnetName = user.NetworkProfile?.DomainName ?: ""
Text(stringResource(id = R.string.connect_to_tailnet, tailnetName), Text(
style = MaterialTheme.typography.bodyMedium, stringResource(id = R.string.connect_to_tailnet, tailnetName),
color = MaterialTheme.colorScheme.primary 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
) )
Button(onClick = connectAction) { Text(text = stringResource(id = R.string.connect)) } }
} else { } else {
Button(onClick = loginAction) { Text(text = stringResource(id = R.string.log_in)) } 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
)
}
}
} }
} }
} }
@ -224,7 +276,8 @@ fun PeerList(searchTerm: StateFlow<String>,
tonalElevation = 2.dp, tonalElevation = 2.dp,
shadowElevation = 2.dp, shadowElevation = 2.dp,
colors = SearchBarDefaults.colors(), colors = SearchBarDefaults.colors(),
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth()
) {
LazyColumn( LazyColumn(
modifier = 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. // 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 isSelfAndRunning = (peer.StableID == selfPeer && stateVal.value == Ipn.State.Running)
val color: Color = if ((peer.Online == true) || isSelfAndRunning) { val color: Color = if ((peer.Online == true) || isSelfAndRunning) {
Color.Green ts_color_light_green
} else { } else {
Color.Gray Color.Gray
} }

@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.Links import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.model.IpnLocal 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.defaultPaddingModifier
import com.tailscale.ipn.ui.util.settingsRowModifier import com.tailscale.ipn.ui.util.settingsRowModifier
import com.tailscale.ipn.ui.viewModel.Setting import com.tailscale.ipn.ui.viewModel.Setting
@ -57,15 +58,17 @@ data class SettingsNav(
fun Settings(viewModel: SettingsViewModel) { fun Settings(viewModel: SettingsViewModel) {
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
Surface(color = MaterialTheme.colorScheme.surface) { Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxHeight()) {
Column(modifier = defaultPaddingModifier().fillMaxHeight()) { Column(modifier = defaultPaddingModifier().fillMaxHeight()) {
Text(text = stringResource(id = R.string.settings_title), Text(
text = stringResource(id = R.string.settings_title),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleMedium) style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -79,7 +82,7 @@ fun Settings(viewModel: SettingsViewModel) {
handler.openUri(Links.ADMIN_URL) handler.openUri(Links.ADMIN_URL)
}) })
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.ipnManager.logout() }) { PrimaryActionButton(onClick = { viewModel.ipnManager.logout() }) {
Text(text = stringResource(id = R.string.log_out)) Text(text = stringResource(id = R.string.log_out))
} }
} ?: run { } ?: run {
@ -93,7 +96,11 @@ fun Settings(viewModel: SettingsViewModel) {
viewModel.settings.forEach { settingBundle -> viewModel.settings.forEach { settingBundle ->
Column(modifier = settingsRowModifier()) { Column(modifier = settingsRowModifier()) {
settingBundle.title?.let { 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 -> settingBundle.settings.forEach { setting ->
when (setting.type) { when (setting.type) {
@ -118,7 +125,12 @@ fun Settings(viewModel: SettingsViewModel) {
} }
@Composable @Composable
fun UserView(profile: IpnLocal.LoginProfile?, isAdmin: Boolean, adminText: AnnotatedString, onClick: () -> Unit) { fun UserView(
profile: IpnLocal.LoginProfile?,
isAdmin: Boolean,
adminText: AnnotatedString,
onClick: () -> Unit
) {
Column { Column {
Row(modifier = settingsRowModifier().padding(8.dp)) { Row(modifier = settingsRowModifier().padding(8.dp)) {
@ -127,15 +139,20 @@ fun UserView(profile: IpnLocal.LoginProfile?, isAdmin: Boolean, adminText: Annot
} }
Column(verticalArrangement = Arrangement.Center) { Column(verticalArrangement = Arrangement.Center) {
Text(text = profile?.UserProfile?.DisplayName Text(
?: "", style = MaterialTheme.typography.titleMedium) text = profile?.UserProfile?.DisplayName
?: "", style = MaterialTheme.typography.titleMedium
)
Text(text = profile?.Name ?: "", style = MaterialTheme.typography.bodyMedium) Text(text = profile?.Name ?: "", style = MaterialTheme.typography.bodyMedium)
} }
} }
if (isAdmin) { if (isAdmin) {
Column(modifier = Modifier.padding(horizontal = 12.dp)) { Column(modifier = Modifier.padding(horizontal = 12.dp)) {
ClickableText(text = adminText, style = MaterialTheme.typography.bodySmall, onClick = { ClickableText(
text = adminText,
style = MaterialTheme.typography.bodySmall,
onClick = {
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.R
import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.service.IpnModel 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.ComposableStringFormatter
import com.tailscale.ipn.ui.util.DisplayAddress import com.tailscale.ipn.ui.util.DisplayAddress
import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.util.TimeUtil
@ -47,6 +48,6 @@ class PeerDetailsViewModel(val model: IpnModel, val nodeId: StableNodeID) : View
nodeName = peer?.ComputedName ?: "" nodeName = peer?.ComputedName ?: ""
connectedStrRes = if (peer?.Online == true) R.string.connected else R.string.not_connected 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="bug_report">Bug Report</string>
<string name="use_ts_dns">Use Tailscale DNS</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="exit_node">Exit Node</string>
<string name="starting">Starting…</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 --> <!-- Strings for peer details -->
<string name="addresses_section">TAILSCALE ADDRESSES</string> <string name="addresses_section">TAILSCALE ADDRESSES</string>

Loading…
Cancel
Save