diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index e87b8bb..aa30b6d 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ 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"> +fun AboutView(nav: BackNavigation) { + Scaffold(topBar = { Header(R.string.about_view_title, onBack = nav.onBack) }) { innerPadding -> Column( verticalArrangement = Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().fillMaxHeight().safeContentPadding()) { + modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(innerPadding)) { Image( modifier = Modifier.width(100.dp) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt index f71e4ca..3b5ecad 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/BugReportView.kt @@ -24,50 +24,53 @@ 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(model: BugReportViewModel = viewModel()) { +fun BugReportView(nav: BackNavigation, model: BugReportViewModel = viewModel()) { val handler = LocalUriHandler.current - 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) }) + 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) }) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) - ReportIdRow(bugReportIdFlow = model.bugReportID) + ReportIdRow(bugReportIdFlow = model.bugReportID) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.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) + } } } @@ -87,7 +90,8 @@ fun ReportIdRow(bugReportIdFlow: StateFlow) { Text( text = bugReportId.value, style = MaterialTheme.typography.titleMedium, - maxLines = 1, + fontFamily = FontFamily.Monospace, + maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = defaultPaddingModifier()) } @@ -105,9 +109,10 @@ fun contactText(): AnnotatedString { } pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL) - withStyle(style = SpanStyle(color = Color.Blue)) { - append(stringResource(id = R.string.bug_report_instructions_linktext)) - } + withStyle( + style = SpanStyle(color = ts_color_light_blue, textDecoration = TextDecoration.Underline)) { + append(stringResource(id = R.string.bug_report_instructions_linktext)) + } pop() withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt index 0be44ef..d291a18 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt @@ -43,7 +43,8 @@ fun ExitNodePicker( model: ExitNodePickerViewModel = viewModel(factory = ExitNodePickerViewModelFactory(nav)) ) { LoadingIndicator.Wrap { - Scaffold(topBar = { Header(R.string.choose_exit_node) }) { innerPadding -> + Scaffold(topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateHome) }) { + innerPadding -> val tailnetExitNodes = model.tailnetExitNodes.collectAsState() val mullvadExitNodes = model.mullvadExitNodesByCountryCode.collectAsState() val anyActive = model.anyActive.collectAsState() diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt index 191f7a9..ab65463 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt @@ -31,8 +31,9 @@ import com.tailscale.ipn.ui.viewModel.IpnViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MDMSettingsDebugView(model: IpnViewModel = viewModel()) { - Scaffold(topBar = { Header(R.string.current_mdm_settings) }) { innerPadding -> +fun MDMSettingsDebugView(nav: BackNavigation, model: IpnViewModel = viewModel()) { + Scaffold(topBar = { Header(R.string.current_mdm_settings, onBack = nav.onBack) }) { innerPadding + -> val mdmSettings = IpnViewModel.mdmSettings.collectAsState().value LazyColumn(modifier = Modifier.padding(innerPadding)) { items(enumValues()) { booleanSetting -> diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index a6f5221..699937f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -10,21 +10,23 @@ 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 @@ -32,7 +34,6 @@ 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 @@ -68,49 +69,61 @@ data class MainViewNavigation( @Composable fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewModel()) { - Scaffold { _ -> - Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Center) { - val state = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState) - val user = viewModel.loggedInUser.collectAsState(initial = null) + 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().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)) - } + 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) + StateDisplay(viewModel.stateRes, viewModel.userName) - 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) - } - } - } + 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) + } + } + } - when (state.value) { - Ipn.State.Running -> { + 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) }) + 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) + } + 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 {} }) + } } - Ipn.State.Starting -> StartingView() - else -> ConnectView(user.value, { viewModel.toggleVpn() }, { viewModel.login {} }) - } - } } } @@ -135,16 +148,17 @@ 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.secondaryContainer) + .background(MaterialTheme.colorScheme.background) .fillMaxWidth()) { - Column(modifier = Modifier.padding(6.dp)) { + Column(modifier = Modifier.padding(vertical = 15.dp, horizontal = 18.dp)) { Text( text = stringResource(id = R.string.exit_node), - style = MaterialTheme.typography.titleMedium) + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleSmall) Row(verticalAlignment = Alignment.CenterVertically) { Text( text = exitNode ?: stringResource(id = R.string.none), - style = MaterialTheme.typography.bodyMedium) + style = MaterialTheme.typography.bodyLarge) Icon( Icons.Outlined.ArrowDropDown, null, @@ -208,62 +222,64 @@ fun StartingView() { fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAction: () -> Unit) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { Column( - modifier = - 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) + 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) + } + } + } } - } - } } } @@ -308,9 +324,11 @@ fun PeerList( trailingIcon = { if (searchTermStr.isNotEmpty()) ClearButton({ onSearch("") }) else CloseButton() }, - tonalElevation = 2.dp, - shadowElevation = 2.dp, - colors = SearchBarDefaults.colors(), + tonalElevation = 0.dp, + shadowElevation = 0.dp, + colors = + SearchBarDefaults.colors( + containerColor = Color.Transparent, dividerColor = Color.Transparent), modifier = Modifier.fillMaxWidth()) { LazyColumn( modifier = @@ -323,7 +341,8 @@ fun PeerList( Text( text = peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user), - style = MaterialTheme.typography.titleLarge) + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold) }) } peerSet.peers.forEach { peer -> @@ -344,19 +363,20 @@ fun PeerList( } Box( modifier = - Modifier.size(8.dp) + Modifier.size(10.dp) .background( color = color, shape = RoundedCornerShape(percent = 50))) {} - Spacer(modifier = Modifier.size(8.dp)) + Spacer(modifier = Modifier.size(6.dp)) Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) } }, supportingContent = { Text( text = peer.Addresses?.first()?.split("/")?.first() ?: "", - style = MaterialTheme.typography.bodyMedium) - }, - trailingContent = { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null) }) + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary) + }) + HorizontalDivider(color = MaterialTheme.colorScheme.secondaryContainer) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt index 147a06b..2b8b108 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ManagedByView.kt @@ -7,8 +7,7 @@ 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.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -22,8 +21,8 @@ import com.tailscale.ipn.mdm.StringSetting import com.tailscale.ipn.ui.viewModel.IpnViewModel @Composable -fun ManagedByView(model: IpnViewModel = viewModel()) { - Surface(color = MaterialTheme.colorScheme.surface) { +fun ManagedByView(nav: BackNavigation, model: IpnViewModel = viewModel()) { + Scaffold(topBar = { Header(R.string.managed_by, onBack = nav.onBack) }) { innerPadding -> Column( verticalArrangement = Arrangement.spacedBy(space = 20.dp, alignment = Alignment.CenterVertically), diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt index be770cf..c601905 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PeerDetails.kt @@ -35,52 +35,55 @@ import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModelFactory @Composable fun PeerDetails( + nav: BackNavigation, nodeId: String, model: PeerDetailsViewModel = viewModel(factory = PeerDetailsViewModelFactory(nodeId)) ) { - 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) + 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) } - } + Column(modifier = settingsRowModifier()) { + model.addresses.forEach { AddressRow(address = it.address, type = it.typeString) } + } - Spacer(modifier = Modifier.size(16.dp)) + 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 @@ -88,25 +91,29 @@ fun AddressRow(address: String, type: String) { val localClipboardManager = LocalClipboardManager.current Row( + verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + Modifier.padding(horizontal = 8.dp, vertical = 8.dp) .clickable(onClick = { localClipboardManager.setText(AnnotatedString(address)) })) { Column { - Text(text = address, style = MaterialTheme.typography.titleMedium) - Text(text = type, style = MaterialTheme.typography.bodyMedium) + Text(text = address) + Text( + text = type, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + color = MaterialTheme.colorScheme.secondary) } Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Icon(Icons.Outlined.Share, null) + Icon(Icons.Outlined.Share, null, tint = MaterialTheme.colorScheme.secondary) } } } @Composable fun ValueRow(title: String, value: String) { - Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).fillMaxWidth()) { - Text(text = title, style = MaterialTheme.typography.titleMedium) + Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp).fillMaxWidth()) { + Text(text = title) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Text(text = value, style = MaterialTheme.typography.bodyMedium) + Text(text = value, color = MaterialTheme.colorScheme.secondary) } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index 8f76556..ef5fbf0 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -15,7 +15,6 @@ 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 @@ -51,29 +50,31 @@ fun Settings( val user = viewModel.loggedInUser.collectAsState().value val isAdmin = viewModel.isAdmin.collectAsState().value - 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) } - } + 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) } + } - 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) } + 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)) + } } - Spacer(modifier = Modifier.height(8.dp)) } - } - } } @Composable @@ -142,7 +143,7 @@ fun SettingRow(setting: Setting) { SettingType.SWITCH -> { Text(setting.title.getString()) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - Switch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled) + TintedSwitch(checked = swVal, onCheckedChange = setting.onToggle, enabled = enabled) } } SettingType.NAV -> { @@ -155,7 +156,6 @@ fun SettingRow(setting: Setting) { Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { Text(text = txtVal, style = MaterialTheme.typography.bodyMedium) } - ChevronRight() } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt index 3624240..02af82c 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SharedViews.kt @@ -4,9 +4,11 @@ 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.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -20,22 +22,31 @@ 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) { +fun Header(@StringRes title: Int, onBack: (() -> Unit)? = null) { TopAppBar( colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceContainer, titleContentColor = MaterialTheme.colorScheme.primary, ), - title = { Text(stringResource(title)) }) + title = { Text(stringResource(title)) }, + navigationIcon = { onBack?.let { BackArrow(action = it) } }, + ) } @Composable -fun ChevronRight() { - Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null) +fun BackArrow(action: () -> Unit) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + null, + modifier = Modifier.clickable { action() }.padding(start = 15.dp, end = 20.dp)) } @Composable diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt new file mode 100644 index 0000000..69f1ccb --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TintedSwitch.kt @@ -0,0 +1,24 @@ +// 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)) +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt index e0af570..5cdf600 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt @@ -25,12 +25,12 @@ import com.tailscale.ipn.ui.viewModel.UserSwitcherViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun UserSwitcherView(viewModel: UserSwitcherViewModel = viewModel()) { +fun UserSwitcherView(nav: BackNavigation, viewModel: UserSwitcherViewModel = viewModel()) { val users = viewModel.loginProfiles.collectAsState().value val currentUser = viewModel.loggedInUser.collectAsState().value - Scaffold(topBar = { Header(R.string.accounts) }) { innerPadding -> + Scaffold(topBar = { Header(R.string.accounts, onBack = nav.onBack) }) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt index 646ed8a..7ec3690 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserView.kt @@ -62,7 +62,7 @@ fun UserView( when (actionState) { UserActionState.CURRENT -> CheckedIndicator() UserActionState.SWITCHING -> SimpleActivityIndicator(size = 26) - UserActionState.NAV -> ChevronRight() + UserActionState.NAV -> Unit UserActionState.NONE -> Unit } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt index 9e1ccf5..77f85b2 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt @@ -78,7 +78,8 @@ data class SettingsNav( val onNavigateToAbout: () -> Unit, val onNavigateToMDMSettings: () -> Unit, val onNavigateToManagedBy: () -> Unit, - val onNavigateToUserSwitcher: () -> Unit + val onNavigateToUserSwitcher: () -> Unit, + val onBackPressed: () -> Unit, ) class SettingsViewModelFactory(private val navigation: SettingsNav) : ViewModelProvider.Factory { diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 3fe5c6b..ad5d193 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -24,13 +24,14 @@ Terms of Service 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. The Tailscale App Icon + Managed By Report a Bug To report a bug,  - contact our support team  - and include the ID below. - This ID helps us find the event ino our diagnostic logs. This process does not share any of your personally-identifiable information. + contact our support team  + and include the ID below. + This ID helps us find the event in our diagnostic logs. This process does not share any of your personally-identifiable information. Settings @@ -41,16 +42,17 @@ Use Tailscale DNS - Exit Node + EXIT NODE Starting… "Connect again to talk to the other devices in the %1$s tailnet." Welcome to Tailscale Log in to join your tailnet and connect your devices. - TAILSCALE ADDRESSES + OS Key Expiry + Tailscale Addresses Current MDM Settings