Revert "[568eb59] android/ui: address preliminary design feedback (#227)"

This reverts commit 910511d838.

Signed-off-by: James Tucker <james@tailscale.com>
pull/236/head
James Tucker 2 months ago committed by James Tucker
parent 0d1a3cf415
commit 5454b34dd1

@ -29,7 +29,6 @@
android:allowBackup="false"
android:banner="@drawable/tv_banner"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.AppCompat"
android:label="Tailscale"
android:roundIcon="@mipmap/ic_launcher_round">
<activity

@ -28,7 +28,6 @@ import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.theme.AppTheme
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.view.AboutView
import com.tailscale.ipn.ui.view.BackNavigation
import com.tailscale.ipn.ui.view.BugReportView
import com.tailscale.ipn.ui.view.ExitNodePicker
import com.tailscale.ipn.ui.view.MDMSettingsDebugView
@ -81,11 +80,8 @@ class MainActivity : ComponentActivity() {
onNavigateToMDMSettings = { navController.navigate("mdmSettings") },
onNavigateToManagedBy = { navController.navigate("managedBy") },
onNavigateToUserSwitcher = { navController.navigate("userSwitcher") },
onBackPressed = { navController.popBackStack() },
)
val backNav = BackNavigation(onBack = { navController.popBackStack() })
val exitNodePickerNav =
ExitNodePickerNav(
onNavigateHome = {
@ -107,13 +103,13 @@ class MainActivity : ComponentActivity() {
composable(
"peerDetails/{nodeId}",
arguments = listOf(navArgument("nodeId") { type = NavType.StringType })) {
PeerDetails(nav = backNav, it.arguments?.getString("nodeId") ?: "")
PeerDetails(it.arguments?.getString("nodeId") ?: "")
}
composable("bugReport") { BugReportView(nav = backNav) }
composable("about") { AboutView(nav = backNav) }
composable("mdmSettings") { MDMSettingsDebugView(nav = backNav) }
composable("managedBy") { ManagedByView(nav = backNav) }
composable("userSwitcher") { UserSwitcherView(nav = backNav) }
composable("bugReport") { BugReportView() }
composable("about") { AboutView() }
composable("mdmSettings") { MDMSettingsDebugView() }
composable("managedBy") { ManagedByView() }
composable("userSwitcher") { UserSwitcherView() }
}
}
}

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@ -31,13 +32,13 @@ import com.tailscale.ipn.R
import com.tailscale.ipn.ui.Links
@Composable
fun AboutView(nav: BackNavigation) {
Scaffold(topBar = { Header(R.string.about_view_title, onBack = nav.onBack) }) { innerPadding ->
fun AboutView() {
Scaffold { _ ->
Column(
verticalArrangement =
Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(innerPadding)) {
modifier = Modifier.fillMaxWidth().fillMaxHeight().safeContentPadding()) {
Image(
modifier =
Modifier.width(100.dp)

@ -24,53 +24,50 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.theme.ts_color_light_blue
import com.tailscale.ipn.ui.util.defaultPaddingModifier
import com.tailscale.ipn.ui.util.settingsRowModifier
import com.tailscale.ipn.ui.viewModel.BugReportViewModel
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) {
fun BugReportView(model: BugReportViewModel = viewModel()) {
val handler = LocalUriHandler.current
Scaffold(topBar = { Header(R.string.bug_report_title, onBack = nav.onBack) }) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding).padding(24.dp).fillMaxWidth().fillMaxHeight()) {
ClickableText(
text = contactText(),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
onClick = { handler.openUri(Links.SUPPORT_URL) })
Scaffold(topBar = { Header(R.string.bug_report_title) }) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).padding(8.dp).fillMaxWidth().fillMaxHeight()) {
ClickableText(
text = contactText(),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
onClick = { handler.openUri(Links.SUPPORT_URL) })
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(8.dp))
ReportIdRow(bugReportIdFlow = model.bugReportID)
ReportIdRow(bugReportIdFlow = model.bugReportID)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.bug_report_id_desc),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Left,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodySmall)
}
Text(
text = stringResource(id = R.string.bug_report_id_desc),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Left,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodySmall)
}
}
}
@ -90,8 +87,7 @@ fun ReportIdRow(bugReportIdFlow: StateFlow<String>) {
Text(
text = bugReportId.value,
style = MaterialTheme.typography.titleMedium,
fontFamily = FontFamily.Monospace,
maxLines = 2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = defaultPaddingModifier())
}
@ -109,10 +105,9 @@ fun contactText(): AnnotatedString {
}
pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL)
withStyle(
style = SpanStyle(color = ts_color_light_blue, textDecoration = TextDecoration.Underline)) {
append(stringResource(id = R.string.bug_report_instructions_linktext))
}
withStyle(style = SpanStyle(color = Color.Blue)) {
append(stringResource(id = R.string.bug_report_instructions_linktext))
}
pop()
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {

@ -43,8 +43,7 @@ fun ExitNodePicker(
model: ExitNodePickerViewModel = viewModel(factory = ExitNodePickerViewModelFactory(nav))
) {
LoadingIndicator.Wrap {
Scaffold(topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateHome) }) {
innerPadding ->
Scaffold(topBar = { Header(R.string.choose_exit_node) }) { innerPadding ->
val tailnetExitNodes = model.tailnetExitNodes.collectAsState()
val mullvadExitNodes = model.mullvadExitNodesByCountryCode.collectAsState()
val anyActive = model.anyActive.collectAsState()

@ -31,9 +31,8 @@ import com.tailscale.ipn.ui.viewModel.IpnViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MDMSettingsDebugView(nav: BackNavigation, model: IpnViewModel = viewModel()) {
Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = nav.onBack) }) { innerPadding
->
fun MDMSettingsDebugView(model: IpnViewModel = viewModel()) {
Scaffold(topBar = { Header(R.string.current_mdm_settings) }) { innerPadding ->
val mdmSettings = IpnViewModel.mdmSettings.collectAsState().value
LazyColumn(modifier = Modifier.padding(innerPadding)) {
items(enumValues<BooleanSetting>()) { booleanSetting ->

@ -10,23 +10,21 @@ 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.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
@ -34,6 +32,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -69,61 +68,49 @@ data class MainViewNavigation(
@Composable
fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewModel()) {
Scaffold(contentWindowInsets = WindowInsets.Companion.statusBars) { paddingInsets ->
Column(
modifier = Modifier.fillMaxWidth().padding(paddingInsets),
verticalArrangement = Arrangement.Center) {
val state = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState)
val user = viewModel.loggedInUser.collectAsState(initial = null)
Row(
modifier =
Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(horizontal = 8.dp)
.padding(top = 10.dp),
verticalAlignment = Alignment.CenterVertically) {
val isOn = viewModel.vpnToggleState.collectAsState(initial = false)
if (state.value != Ipn.State.NeedsLogin && state.value != Ipn.State.NoState) {
TintedSwitch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value)
Spacer(Modifier.size(3.dp))
}
StateDisplay(viewModel.stateRes, viewModel.userName)
Scaffold { _ ->
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Center) {
val state = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState)
val user = viewModel.loggedInUser.collectAsState(initial = null)
Box(
modifier = Modifier.weight(1f).clickable { navigation.onNavigateToSettings() },
contentAlignment = Alignment.CenterEnd) {
when (user.value) {
null -> SettingsButton(user.value) { navigation.onNavigateToSettings() }
else -> Avatar(profile = user.value, size = 36)
}
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically) {
val isOn = viewModel.vpnToggleState.collectAsState(initial = false)
if (state.value != Ipn.State.NeedsLogin && state.value != Ipn.State.NoState) {
Switch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value)
Spacer(Modifier.size(3.dp))
}
when (state.value) {
Ipn.State.Running -> {
StateDisplay(viewModel.stateRes, viewModel.userName)
val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "")
Row(
modifier =
Modifier.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(top = 10.dp, bottom = 20.dp)) {
ExitNodeStatus(
navAction = navigation.onNavigateToExitNodes, viewModel = viewModel)
Box(
modifier = Modifier.weight(1f).clickable { navigation.onNavigateToSettings() },
contentAlignment = Alignment.CenterEnd) {
when (user.value) {
null -> SettingsButton(user.value) { navigation.onNavigateToSettings() }
else -> Avatar(profile = user.value, size = 36)
}
PeerList(
searchTerm = viewModel.searchTerm,
state = viewModel.ipnState,
peers = viewModel.peers,
selfPeer = selfPeerId.value,
onNavigateToPeerDetails = navigation.onNavigateToPeerDetails,
onSearch = { viewModel.searchPeers(it) })
}
Ipn.State.Starting -> StartingView()
else -> ConnectView(user.value, { viewModel.toggleVpn() }, { viewModel.login {} })
}
}
when (state.value) {
Ipn.State.Running -> {
val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "")
ExitNodeStatus(navAction = navigation.onNavigateToExitNodes, viewModel = viewModel)
PeerList(
searchTerm = viewModel.searchTerm,
state = viewModel.ipnState,
peers = viewModel.peers,
selfPeer = selfPeerId.value,
onNavigateToPeerDetails = navigation.onNavigateToPeerDetails,
onSearch = { viewModel.searchPeers(it) })
}
Ipn.State.Starting -> StartingView()
else -> ConnectView(user.value, { viewModel.toggleVpn() }, { viewModel.login {} })
}
}
}
}
@ -148,17 +135,16 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
Modifier.clickable { navAction() }
.padding(horizontal = 8.dp)
.clip(shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp))
.background(MaterialTheme.colorScheme.background)
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth()) {
Column(modifier = Modifier.padding(vertical = 15.dp, horizontal = 18.dp)) {
Column(modifier = Modifier.padding(6.dp)) {
Text(
text = stringResource(id = R.string.exit_node),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall)
style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = exitNode ?: stringResource(id = R.string.none),
style = MaterialTheme.typography.bodyLarge)
style = MaterialTheme.typography.bodyMedium)
Icon(
Icons.Outlined.ArrowDropDown,
null,
@ -222,64 +208,62 @@ fun StartingView() {
fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.background(MaterialTheme.colorScheme.secondaryContainer).fillMaxWidth()) {
Column(
modifier = Modifier.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)
}
}
}
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)
}
}
}
}
}
@ -324,11 +308,9 @@ fun PeerList(
trailingIcon = {
if (searchTermStr.isNotEmpty()) ClearButton({ onSearch("") }) else CloseButton()
},
tonalElevation = 0.dp,
shadowElevation = 0.dp,
colors =
SearchBarDefaults.colors(
containerColor = Color.Transparent, dividerColor = Color.Transparent),
tonalElevation = 2.dp,
shadowElevation = 2.dp,
colors = SearchBarDefaults.colors(),
modifier = Modifier.fillMaxWidth()) {
LazyColumn(
modifier =
@ -341,8 +323,7 @@ fun PeerList(
Text(
text =
peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold)
style = MaterialTheme.typography.titleLarge)
})
}
peerSet.peers.forEach { peer ->
@ -363,20 +344,19 @@ fun PeerList(
}
Box(
modifier =
Modifier.size(10.dp)
Modifier.size(8.dp)
.background(
color = color, shape = RoundedCornerShape(percent = 50))) {}
Spacer(modifier = Modifier.size(6.dp))
Spacer(modifier = Modifier.size(8.dp))
Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium)
}
},
supportingContent = {
Text(
text = peer.Addresses?.first()?.split("/")?.first() ?: "",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary)
})
HorizontalDivider(color = MaterialTheme.colorScheme.secondaryContainer)
style = MaterialTheme.typography.bodyMedium)
},
trailingContent = { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null) })
}
}
}

@ -7,7 +7,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -21,8 +22,8 @@ import com.tailscale.ipn.mdm.StringSetting
import com.tailscale.ipn.ui.viewModel.IpnViewModel
@Composable
fun ManagedByView(nav: BackNavigation, model: IpnViewModel = viewModel()) {
Scaffold(topBar = { Header(R.string.managed_by, onBack = nav.onBack) }) { innerPadding ->
fun ManagedByView(model: IpnViewModel = viewModel()) {
Surface(color = MaterialTheme.colorScheme.surface) {
Column(
verticalArrangement =
Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically),

@ -35,55 +35,52 @@ import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory
@Composable
fun PeerDetails(
nav: BackNavigation,
nodeId: String,
model: PeerDetailsViewModel = viewModel(factory = PeerDetailsViewModelFactory(nodeId))
) {
Scaffold(topBar = { Header(title = R.string.peer_details, onBack = nav.onBack) }) { innerPadding
->
Column(
modifier =
Modifier.fillMaxWidth()
.padding(innerPadding)
.padding(horizontal = 16.dp)
.padding(top = 22.dp),
) {
Text(
text = model.nodeName,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary)
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier =
Modifier.size(8.dp)
.background(
color = model.connectedColor, shape = RoundedCornerShape(percent = 50))) {}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = model.connectedStrRes),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary)
}
Column(modifier = Modifier.fillMaxHeight()) {
Text(
text = stringResource(id = R.string.addresses_section),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary)
Column(modifier = settingsRowModifier()) {
model.addresses.forEach { AddressRow(address = it.address, type = it.typeString) }
Scaffold(
topBar = {
Column(
modifier = Modifier.fillMaxWidth().padding(8.dp),
) {
Text(
text = model.nodeName,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary)
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier =
Modifier.size(8.dp)
.background(
color = model.connectedColor,
shape = RoundedCornerShape(percent = 50))) {}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = model.connectedStrRes),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary)
}
}
}) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxHeight()) {
Text(
text = stringResource(id = R.string.addresses_section),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.size(16.dp))
Column(modifier = settingsRowModifier()) {
model.addresses.forEach { AddressRow(address = it.address, type = it.typeString) }
}
Spacer(modifier = Modifier.size(16.dp))
Column(modifier = settingsRowModifier()) {
model.info.forEach {
ValueRow(title = stringResource(id = it.titleRes), value = it.value.getString())
Column(modifier = settingsRowModifier()) {
model.info.forEach {
ValueRow(title = stringResource(id = it.titleRes), value = it.value.getString())
}
}
}
}
}
}
}
@Composable
@ -91,29 +88,25 @@ fun AddressRow(address: String, type: String) {
val localClipboardManager = LocalClipboardManager.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
.clickable(onClick = { localClipboardManager.setText(AnnotatedString(address)) })) {
Column {
Text(text = address)
Text(
text = type,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
color = MaterialTheme.colorScheme.secondary)
Text(text = address, style = MaterialTheme.typography.titleMedium)
Text(text = type, style = MaterialTheme.typography.bodyMedium)
}
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Icon(Icons.Outlined.Share, null, tint = MaterialTheme.colorScheme.secondary)
Icon(Icons.Outlined.Share, null)
}
}
}
@Composable
fun ValueRow(title: String, value: String) {
Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp).fillMaxWidth()) {
Text(text = title)
Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).fillMaxWidth()) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Text(text = value, color = MaterialTheme.colorScheme.secondary)
Text(text = value, style = MaterialTheme.typography.bodyMedium)
}
}
}

@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -50,31 +51,29 @@ fun Settings(
val user = viewModel.loggedInUser.collectAsState().value
val isAdmin = viewModel.isAdmin.collectAsState().value
Scaffold(
topBar = { Header(title = R.string.settings_title, onBack = settingsNav.onBackPressed) }) {
innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxHeight().padding(16.dp)) {
UserView(
profile = user,
actionState = UserActionState.NAV,
onClick = viewModel.navigation.onNavigateToUserSwitcher)
if (isAdmin) {
Spacer(modifier = Modifier.height(4.dp))
AdminTextView { handler.openUri(Links.ADMIN_URL) }
}
Scaffold(topBar = { Header(title = R.string.settings_title) }) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxHeight()) {
UserView(
profile = user,
actionState = UserActionState.NAV,
onClick = viewModel.navigation.onNavigateToUserSwitcher)
if (isAdmin) {
Spacer(modifier = Modifier.height(4.dp))
AdminTextView { handler.openUri(Links.ADMIN_URL) }
}
Spacer(modifier = Modifier.height(8.dp))
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))
}
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
@ -143,7 +142,7 @@ fun SettingRow(setting: Setting) {
SettingType.SWITCH -> {
Text(setting.title.getString())
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
TintedSwitch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled)
Switch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled)
}
}
SettingType.NAV -> {
@ -156,6 +155,7 @@ fun SettingRow(setting: Setting) {
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Text(text = txtVal, style = MaterialTheme.typography.bodyMedium)
}
ChevronRight()
}
}
}

@ -4,11 +4,9 @@
package com.tailscale.ipn.ui.view
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@ -22,31 +20,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
data class BackNavigation(
val onBack: () -> Unit,
)
// Header view for all secondary screens
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Header(@StringRes title: Int, onBack: (() -> Unit)? = null) {
fun Header(@StringRes title: Int) {
TopAppBar(
colors =
TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = { Text(stringResource(title)) },
navigationIcon = { onBack?.let { BackArrow(action = it) } },
)
title = { Text(stringResource(title)) })
}
@Composable
fun BackArrow(action: () -> Unit) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
null,
modifier = Modifier.clickable { action() }.padding(start = 15.dp, end = 20.dp))
fun ChevronRight() {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null)
}
@Composable

@ -1,24 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.view
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import com.tailscale.ipn.ui.theme.ts_color_light_blue
@Composable
fun TintedSwitch(checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, enabled: Boolean = true) {
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
colors =
SwitchDefaults.colors(
checkedBorderColor = ts_color_light_blue,
checkedThumbColor = ts_color_light_blue,
checkedTrackColor = ts_color_light_blue.copy(alpha = 0.3f),
uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer))
}

@ -25,12 +25,12 @@ import com.tailscale.ipn.ui.viewModel.UserSwitcherViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserSwitcherView(nav: BackNavigation, viewModel: UserSwitcherViewModel = viewModel()) {
fun UserSwitcherView(viewModel: UserSwitcherViewModel = viewModel()) {
val users = viewModel.loginProfiles.collectAsState().value
val currentUser = viewModel.loggedInUser.collectAsState().value
Scaffold(topBar = { Header(R.string.accounts, onBack = nav.onBack) }) { innerPadding ->
Scaffold(topBar = { Header(R.string.accounts) }) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)) {

@ -62,7 +62,7 @@ fun UserView(
when (actionState) {
UserActionState.CURRENT -> CheckedIndicator()
UserActionState.SWITCHING -> SimpleActivityIndicator(size = 26)
UserActionState.NAV -> Unit
UserActionState.NAV -> ChevronRight()
UserActionState.NONE -> Unit
}
}

@ -78,8 +78,7 @@ data class SettingsNav(
val onNavigateToAbout: () -> Unit,
val onNavigateToMDMSettings: () -> Unit,
val onNavigateToManagedBy: () -> Unit,
val onNavigateToUserSwitcher: () -> Unit,
val onBackPressed: () -> Unit,
val onNavigateToUserSwitcher: () -> Unit
)
class SettingsViewModelFactory(private val navigation: SettingsNav) : ViewModelProvider.Factory {

@ -24,14 +24,13 @@
<string name="terms_of_service">Terms of Service</string>
<string name="about_view_footnotes">WireGuard is a registered trademark of Jason A. Donenfeld.\n\n© 2024 Tailscale Inc. All rights reserved.\nTailscale is a registered trademark of Tailscale Inc.</string>
<string name="app_icon_content_description">The Tailscale App Icon</string>
<string name="managed_by">Managed By</string>
<!-- Strings for the bug reporting screen -->
<string name="bug_report_title">Report a Bug</string>
<string name="bug_report_instructions_prefix">To report a bug,&#160;</string>
<string name="bug_report_instructions_linktext">contact our support team&#160;</string>
<string name="bug_report_instructions_suffix"> and include the ID below.</string>
<string name="bug_report_id_desc">This ID helps us find the event in our diagnostic logs. This process does not share any of your personally-identifiable information.</string>
<string name="bug_report_instructions_linktext">contact our support team&#160; </string>
<string name="bug_report_instructions_suffix">and include the ID below.</string>
<string name="bug_report_id_desc">This ID helps us find the event ino our diagnostic logs. This process does not share any of your personally-identifiable information.</string>
<!-- Strings for the settings screen -->
<string name="settings_title">Settings</string>
@ -42,17 +41,16 @@
<string name="use_ts_dns">Use Tailscale DNS</string>
<!-- 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="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"></string>
<string name="addresses_section">TAILSCALE ADDRESSES</string>
<string name="os">OS</string>
<string name="key_expiry">Key Expiry</string>
<string name="peer_details">Tailscale Addresses</string>
<!-- Strings for MDM settings -->
<string name="current_mdm_settings">Current MDM Settings</string>

Loading…
Cancel
Save