android/ui: updated main settings screen to material design

Updates #cleanup

Signed-off-by: Percy Wegmann <percy@tailscale.com>
pull/251/head
Percy Wegmann 2 years ago committed by Percy Wegmann
parent 9f3e871637
commit 7392c7086e

@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp
object Lists { object Lists {
@Composable @Composable
fun SectionDivider() { fun SectionDivider() {
Box(Modifier.size(0.dp, 24.dp)) Box(Modifier.size(0.dp, 8.dp))
} }
@Composable @Composable

@ -4,26 +4,23 @@
package com.tailscale.ipn.ui.view package com.tailscale.ipn.ui.view
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
@ -31,10 +28,9 @@ 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.Links import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.theme.ts_color_dark_desctrutive_text import com.tailscale.ipn.ui.theme.ts_color_dark_desctrutive_text
import com.tailscale.ipn.ui.util.defaultPaddingModifier import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.settingsRowModifier import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.Setting import com.tailscale.ipn.ui.viewModel.Setting
import com.tailscale.ipn.ui.viewModel.SettingType import com.tailscale.ipn.ui.viewModel.SettingType
import com.tailscale.ipn.ui.viewModel.SettingsNav import com.tailscale.ipn.ui.viewModel.SettingsNav
@ -49,113 +45,76 @@ fun Settings(
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
val user = viewModel.loggedInUser.collectAsState().value val user = viewModel.loggedInUser.collectAsState().value
val isAdmin = viewModel.isAdmin.collectAsState().value val isAdmin = viewModel.isAdmin.collectAsState().value
val settingsBundles = viewModel.settings.collectAsState().value
Scaffold( Scaffold(
topBar = { Header(title = R.string.settings_title, onBack = settingsNav.onBackPressed) }) { topBar = { Header(title = R.string.settings_title, onBack = settingsNav.onBackPressed) }) {
innerPadding -> innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxHeight().padding(16.dp)) { LazyColumn(modifier = Modifier.padding(innerPadding)) {
item {
UserView( UserView(
profile = user, profile = user,
actionState = UserActionState.NAV, actionState = UserActionState.NAV,
onClick = viewModel.navigation.onNavigateToUserSwitcher) onClick = viewModel.navigation.onNavigateToUserSwitcher)
}
if (isAdmin) { if (isAdmin) {
item {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
AdminTextView { handler.openUri(Links.ADMIN_URL) } AdminTextView { handler.openUri(Links.ADMIN_URL) }
} }
Spacer(modifier = Modifier.height(8.dp))
val settings = viewModel.settings.collectAsState().value
settings.forEach { settingBundle ->
Column(modifier = settingsRowModifier()) {
settingBundle.title?.let { SettingTitle(it) }
settingBundle.settings.forEach { SettingRow(it) }
}
Spacer(modifier = Modifier.height(8.dp))
} }
}
}
}
@Composable settingsBundles.forEach { bundle ->
fun UserView( item { Lists.SectionDivider() }
profile: IpnLocal.LoginProfile?,
isAdmin: Boolean,
adminText: AnnotatedString,
onClick: () -> Unit
) {
Column {
Row(modifier = settingsRowModifier().padding(8.dp)) {
Box(modifier = defaultPaddingModifier()) { Avatar(profile = profile, size = 36) }
Column(verticalArrangement = Arrangement.Center) {
Text(
text = profile?.UserProfile?.DisplayName ?: "",
style = MaterialTheme.typography.titleMedium)
Text(text = profile?.Name ?: "", style = MaterialTheme.typography.bodyMedium)
}
}
if (isAdmin) { itemsWithDividers(bundle.settings) { setting -> SettingRow(setting) }
Column(modifier = Modifier.padding(horizontal = 12.dp)) {
ClickableText(
text = adminText, style = MaterialTheme.typography.bodySmall, onClick = { onClick() })
} }
} }
} }
} }
@Composable
fun SettingTitle(title: String) {
Text(
text = title, style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(8.dp))
}
@Composable @Composable
fun SettingRow(setting: Setting) { fun SettingRow(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
val txtVal = setting.value?.collectAsState()?.value ?: "" val txtVal = setting.value?.collectAsState()?.value
Row( Box {
modifier = defaultPaddingModifier().clickable { if (enabled) setting.onClick() },
verticalAlignment = Alignment.CenterVertically) {
when (setting.type) { when (setting.type) {
SettingType.NAV_WITH_TEXT -> { SettingType.TEXT ->
Text( ListItem(
setting.title.getString(), modifier = Modifier.clickable { if (enabled) setting.onClick() },
style = MaterialTheme.typography.bodyMedium, headlineContent = {
color =
if (setting.destructive) ts_color_dark_desctrutive_text
else MaterialTheme.colorScheme.primary)
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Text(text = txtVal, style = MaterialTheme.typography.bodyMedium)
}
}
SettingType.TEXT -> {
Text( Text(
setting.title.getString(), setting.title.getString(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = color =
if (setting.destructive) ts_color_dark_desctrutive_text if (setting.destructive) ts_color_dark_desctrutive_text
else MaterialTheme.colorScheme.primary) else MaterialTheme.colorScheme.primary)
} },
SettingType.SWITCH -> { )
Text(setting.title.getString()) SettingType.SWITCH ->
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { ListItem(
modifier = Modifier.clickable { if (enabled) setting.onClick() },
headlineContent = { Text(setting.title.getString()) },
trailingContent = {
TintedSwitch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled) TintedSwitch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled)
} })
}
SettingType.NAV -> { SettingType.NAV -> {
ListItem(
modifier = Modifier.clickable { if (enabled) setting.onClick() },
headlineContent = {
Text( Text(
setting.title.getString(), setting.title.getString(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = color =
if (setting.destructive) ts_color_dark_desctrutive_text if (setting.destructive) ts_color_dark_desctrutive_text
else MaterialTheme.colorScheme.primary) else MaterialTheme.colorScheme.primary)
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { },
Text(text = txtVal, style = MaterialTheme.typography.bodyMedium) supportingContent = {
} txtVal?.let { Text(text = it, style = MaterialTheme.typography.bodyMedium) }
})
} }
} }
} }

@ -4,20 +4,15 @@
package com.tailscale.ipn.ui.view package com.tailscale.ipn.ui.view
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.material3.ListItem
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.util.defaultPaddingModifier
import com.tailscale.ipn.ui.util.settingsRowModifier
// Used to decorate UserViews. // Used to decorate UserViews.
// NONE indicates no decoration // NONE indicates no decoration
@ -37,34 +32,29 @@ fun UserView(
onClick: () -> Unit = {}, onClick: () -> Unit = {},
actionState: UserActionState = UserActionState.NONE actionState: UserActionState = UserActionState.NONE
) { ) {
Column { Box {
Row(
modifier = settingsRowModifier().clickable { onClick() },
verticalAlignment = Alignment.CenterVertically) {
profile?.let { profile?.let {
Box(modifier = defaultPaddingModifier()) { Avatar(profile = profile, size = 36) } ListItem(
modifier = Modifier.clickable { onClick() },
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) { leadingContent = { Avatar(profile = profile, size = 36) },
headlineContent = {
Text( Text(
text = profile.UserProfile.DisplayName, text = profile.UserProfile.DisplayName,
style = MaterialTheme.typography.titleMedium) style = MaterialTheme.typography.titleMedium)
},
supportingContent = {
Text(text = profile.Name, style = MaterialTheme.typography.bodyMedium) Text(text = profile.Name, style = MaterialTheme.typography.bodyMedium)
} },
)
} }
?: run { ?: run {
Box(modifier = Modifier.weight(1f)) { ListItem(
modifier = Modifier.clickable { onClick() },
headlineContent = {
Text( Text(
text = stringResource(id = R.string.accounts), text = stringResource(id = R.string.accounts),
style = MaterialTheme.typography.titleMedium) style = MaterialTheme.typography.titleMedium)
} })
}
when (actionState) {
UserActionState.CURRENT -> CheckedIndicator()
UserActionState.SWITCHING -> SimpleActivityIndicator(size = 26)
UserActionState.NAV -> Unit
UserActionState.NONE -> Unit
}
} }
} }
} }

@ -22,7 +22,6 @@ import kotlinx.coroutines.launch
enum class SettingType { enum class SettingType {
NAV, NAV,
SWITCH, SWITCH,
NAV_WITH_TEXT,
TEXT TEXT
} }

Loading…
Cancel
Save