android/ui: DNS and other styling tweaks

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
jonathan/runasexitnode
Percy Wegmann 1 month ago committed by Percy Wegmann
parent a0e7777958
commit c0ffd5016b

@ -71,9 +71,11 @@ private val LightColors =
surfaceContainer = Color(0xFFF7F5F4), // gray-100
surfaceContainerHigh = Color(0xFFF7F5F4), // gray-100
surfaceContainerHighest = Color(0xFFF7F5F4), // gray-100
surfaceVariant = Color(0xFFF7F5F4), // gray-100,
onSurface = Color(0xFF232222), // gray-800
onSurfaceVariant = Color(0xFF706E6D), // gray-500
outline = Color(0xFFD9D6D5), // gray-300
// outline = Color(0xFFD9D6D5), // gray-300
outline = Color(0xFF706E6D), // gray-500
outlineVariant = Color(0xFFEDEBEA), // gray-200
inverseSurface = Color(0xFF232222), // gray-800
inverseOnSurface = Color(0xFFFFFFFF), // white

@ -17,26 +17,18 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.theme.titledListItem
@Composable
fun ClipboardValueView(
value: String,
title: String? = null,
subtitle: String? = null,
fontFamily: FontFamily = FontFamily.Monospace
) {
fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) {
val localClipboardManager = LocalClipboardManager.current
ListItem(
colors = MaterialTheme.colorScheme.titledListItem,
modifier = Modifier.clickable { localClipboardManager.setText(AnnotatedString(value)) },
overlineContent = { title?.let { Text(it, style = MaterialTheme.typography.titleMedium) } },
headlineContent = {
Text(text = value, style = MaterialTheme.typography.bodyMedium, fontFamily = fontFamily)
},
headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) },
supportingContent = {
subtitle?.let { subtitle ->
Text(

@ -1,19 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.util
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
// FeatureStateRepresentation represents the status of a feature
// in the UI, by providing a symbol, a title, and a caption.
// It is typically implemented as an enumeration.
interface FeatureStateRepresentation {
@get:DrawableRes val symbolDrawable: Int
@get:Composable val tint: Color
@get:StringRes val title: Int
@get:StringRes val caption: Int
}

@ -38,7 +38,7 @@ object LoadingIndicator {
content()
val isLoading = loading.collectAsState().value
if (isLoading) {
Box(Modifier.clickable {}.matchParentSize().background(Color.Gray.copy(alpha = 0.5f)))
Box(Modifier.clickable {}.matchParentSize().background(Color.Gray.copy(alpha = 0.0f)))
Column(
modifier = Modifier.fillMaxWidth(),

@ -5,12 +5,19 @@ package com.tailscale.ipn.ui.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
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.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.DnsType
@ -41,10 +48,22 @@ fun DNSSettingsView(
Scaffold(topBar = { Header(R.string.dns_settings, onBack = nav.onBack) }) { innerPadding ->
LoadingIndicator.Wrap {
LazyColumn(Modifier.padding(innerPadding)) {
item("state") { FeatureStateView(state) }
item("state") {
ListItem(
leadingContent = {
Icon(
painter = painterResource(state.symbolDrawable),
contentDescription = null,
tint = state.tint(),
modifier = Modifier.size(36.dp))
},
headlineContent = {
Text(stringResource(state.title), style = MaterialTheme.typography.titleMedium)
},
supportingContent = { Text(stringResource(state.caption)) })
Lists.ItemDivider()
item("toggle") {
Lists.SectionDivider()
SettingRow(model.useDNSSetting)
}

@ -1,34 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.view
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.ui.util.FeatureStateRepresentation
// FeatureStateView is a Composable that displays the contents of
// a FeatureStateRepresentation.
@Composable
fun FeatureStateView(state: FeatureStateRepresentation) {
ListItem(
leadingContent = {
Icon(
painter = painterResource(state.symbolDrawable),
contentDescription = null,
tint = state.tint,
modifier = Modifier.size(36.dp))
},
headlineContent = {
Text(stringResource(state.title), style = MaterialTheme.typography.titleMedium)
},
supportingContent = { Text(stringResource(state.caption)) })
}

@ -107,10 +107,7 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
colors = MaterialTheme.colorScheme.surfaceContainerListItem,
leadingContent = {
val isOn = viewModel.vpnToggleState.collectAsState(initial = false)
TintedSwitch(
onCheckedChange = { viewModel.toggleVpn() },
checked = isOn.value,
enabled = state != Ipn.State.NoState)
TintedSwitch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value)
},
headlineContent = {
user?.NetworkProfile?.DomainName?.let { domain ->

@ -104,8 +104,12 @@ fun SettingRow(setting: Setting) {
@Composable
private fun TextRow(setting: Setting) {
val enabled = setting.enabled.collectAsState().value
var modifier: Modifier = Modifier
if (enabled) {
setting.onClick?.let { modifier = modifier.clickable(onClick = it) }
}
ListItem(
modifier = Modifier.clickable { if (enabled) setting.onClick() },
modifier = modifier,
colors = MaterialTheme.colorScheme.listItem,
headlineContent = {
Text(
@ -120,8 +124,12 @@ private fun TextRow(setting: Setting) {
private fun SwitchRow(setting: Setting) {
val enabled = setting.enabled.collectAsState().value
val swVal = setting.isOn?.collectAsState()?.value ?: false
var modifier: Modifier = Modifier
if (enabled) {
setting.onClick?.let { modifier = modifier.clickable(onClick = it) }
}
ListItem(
modifier = Modifier.clickable { if (enabled) setting.onClick() },
modifier = modifier,
colors = MaterialTheme.colorScheme.listItem,
headlineContent = {
Text(
@ -136,8 +144,10 @@ private fun SwitchRow(setting: Setting) {
@Composable
private fun NavRow(setting: Setting) {
var modifier: Modifier = Modifier
setting.onClick?.let { modifier = modifier.clickable(onClick = it) }
ListItem(
modifier = Modifier.clickable { setting.onClick() },
modifier = modifier,
colors = MaterialTheme.colorScheme.listItem,
headlineContent = {
Text(

@ -18,7 +18,6 @@ import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.theme.off
import com.tailscale.ipn.ui.theme.success
import com.tailscale.ipn.ui.util.FeatureStateRepresentation
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -84,45 +83,25 @@ class DNSSettingsViewModel() : IpnViewModel() {
}
}
enum class DNSEnablementState : FeatureStateRepresentation {
NOT_RUNNING {
override val title: Int
@StringRes get() = R.string.not_running
override val caption: Int
@StringRes
get() = R.string.tailscale_is_not_running_this_device_is_using_the_system_dns_resolver
override val tint: Color
@Composable get() = MaterialTheme.colorScheme.off
override val symbolDrawable: Int
get() = R.drawable.xmark_circle
},
ENABLED {
override val title: Int
@StringRes get() = R.string.using_tailscale_dns
override val caption: Int
@StringRes get() = R.string.this_device_is_using_tailscale_to_resolve_dns_names
override val tint: Color
@Composable get() = MaterialTheme.colorScheme.success
override val symbolDrawable: Int
get() = R.drawable.check_circle
},
DISABLED {
override val title: Int
@StringRes get() = R.string.not_using_tailscale_dns
override val caption: Int
@StringRes get() = R.string.this_device_is_using_the_system_dns_resolver
override val tint: Color
@Composable get() = MaterialTheme.colorScheme.error
override val symbolDrawable: Int
get() = R.drawable.xmark_circle
}
enum class DNSEnablementState(
@StringRes val title: Int,
@StringRes val caption: Int,
val symbolDrawable: Int,
val tint: @Composable () -> Color
) {
NOT_RUNNING(
R.string.not_running,
R.string.tailscale_is_not_running_this_device_is_using_the_system_dns_resolver,
R.drawable.xmark_circle,
{ MaterialTheme.colorScheme.off }),
ENABLED(
R.string.using_tailscale_dns,
R.string.this_device_is_using_tailscale_to_resolve_dns_names,
R.drawable.check_circle,
{ MaterialTheme.colorScheme.success }),
DISABLED(
R.string.not_using_tailscale_dns,
R.string.this_device_is_using_the_system_dns_resolver,
R.drawable.xmark_circle,
{ MaterialTheme.colorScheme.error })
}

@ -40,8 +40,8 @@ data class Setting(
val destructive: Boolean = false,
val enabled: StateFlow<Boolean> = MutableStateFlow(true),
val isOn: StateFlow<Boolean?>? = null,
val onClick: () -> Unit = {},
val onToggle: (Boolean) -> Unit = {},
val onClick: (() -> Unit)? = null,
val onToggle: (Boolean) -> Unit = {}
)
data class SettingsNav(

Loading…
Cancel
Save