android/ui: DNS and other styling tweaks

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
pull/286/head
Percy Wegmann 3 months ago
parent a0e7777958
commit 4ff2b1c216
No known key found for this signature in database
GPG Key ID: 29D8CDEB4C13D48B

@ -71,9 +71,11 @@ private val LightColors =
surfaceContainer = Color(0xFFF7F5F4), // gray-100 surfaceContainer = Color(0xFFF7F5F4), // gray-100
surfaceContainerHigh = Color(0xFFF7F5F4), // gray-100 surfaceContainerHigh = Color(0xFFF7F5F4), // gray-100
surfaceContainerHighest = Color(0xFFF7F5F4), // gray-100 surfaceContainerHighest = Color(0xFFF7F5F4), // gray-100
surfaceVariant = Color(0xFFF7F5F4), // gray-100,
onSurface = Color(0xFF232222), // gray-800 onSurface = Color(0xFF232222), // gray-800
onSurfaceVariant = Color(0xFF706E6D), // gray-500 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 outlineVariant = Color(0xFFEDEBEA), // gray-200
inverseSurface = Color(0xFF232222), // gray-800 inverseSurface = Color(0xFF232222), // gray-800
inverseOnSurface = Color(0xFFFFFFFF), // white 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
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.theme.titledListItem import com.tailscale.ipn.ui.theme.titledListItem
@Composable @Composable
fun ClipboardValueView( fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) {
value: String,
title: String? = null,
subtitle: String? = null,
fontFamily: FontFamily = FontFamily.Monospace
) {
val localClipboardManager = LocalClipboardManager.current val localClipboardManager = LocalClipboardManager.current
ListItem( ListItem(
colors = MaterialTheme.colorScheme.titledListItem, colors = MaterialTheme.colorScheme.titledListItem,
modifier = Modifier.clickable { localClipboardManager.setText(AnnotatedString(value)) }, modifier = Modifier.clickable { localClipboardManager.setText(AnnotatedString(value)) },
overlineContent = { title?.let { Text(it, style = MaterialTheme.typography.titleMedium) } }, overlineContent = { title?.let { Text(it, style = MaterialTheme.typography.titleMedium) } },
headlineContent = { headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) },
Text(text = value, style = MaterialTheme.typography.bodyMedium, fontFamily = fontFamily)
},
supportingContent = { supportingContent = {
subtitle?.let { subtitle -> subtitle?.let { subtitle ->
Text( 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() content()
val isLoading = loading.collectAsState().value val isLoading = loading.collectAsState().value
if (isLoading) { 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( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

@ -5,12 +5,19 @@ package com.tailscale.ipn.ui.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn 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.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.DnsType import com.tailscale.ipn.ui.model.DnsType
@ -41,10 +48,22 @@ fun DNSSettingsView(
Scaffold(topBar = { Header(R.string.dns_settings, onBack = nav.onBack) }) { innerPadding -> Scaffold(topBar = { Header(R.string.dns_settings, onBack = nav.onBack) }) { innerPadding ->
LoadingIndicator.Wrap { LoadingIndicator.Wrap {
LazyColumn(Modifier.padding(innerPadding)) { 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) 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, colors = MaterialTheme.colorScheme.surfaceContainerListItem,
leadingContent = { leadingContent = {
val isOn = viewModel.vpnToggleState.collectAsState(initial = false) val isOn = viewModel.vpnToggleState.collectAsState(initial = false)
TintedSwitch( TintedSwitch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value)
onCheckedChange = { viewModel.toggleVpn() },
checked = isOn.value,
enabled = state != Ipn.State.NoState)
}, },
headlineContent = { headlineContent = {
user?.NetworkProfile?.DomainName?.let { domain -> user?.NetworkProfile?.DomainName?.let { domain ->

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

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

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

Loading…
Cancel
Save