From 16ec19757d5693a412869dd431998f2c162984e3 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Thu, 14 Mar 2024 08:35:40 -0400 Subject: [PATCH] android: adds support for user avatars and some general cleanup (#202) * android: show user avatars and styling fixes Updates tailscale/corp#18202 fixes ENG-2852 Load and show the user avatar in the right places. There's a universal Avatar composable for this that should work everywhere we need it. This uses the coil-compose lib which seems to be standard practice and will handle caching for us. Restyles a few headers to match the about screen and corrects some layout issues with the height of columns. Signed-off-by: Jonathan Nobels * android: add localizations and view model cleanup to match IPNManager Updates tailscale/corp#18202 Simplifies the view models a bit for readability and localizes a few things that weren't previously localized Signed-off-by: Jonathan Nobels * android: fix peer categorization Updates tailscale/corp#18202 Fixes a null predicate issue for searching and removes the self nodes if there are no matches. Signed-off-by: Jonathan Nobels * android: rename avatar loader to avatar and add header Updates tailscale/corp#18202 Rename the AvatarLoader class to Avatar and move it to views. Add the proper headers. Signed-off-by: Jonathan Nobels --------- Signed-off-by: Jonathan Nobels Co-authored-by: Andrea Gottardo --- android/build.gradle | 2 + .../java/com/tailscale/ipn/MainActivity.kt | 2 +- .../com/tailscale/ipn/ui/util/PeerHelper.kt | 10 +++- .../com/tailscale/ipn/ui/util/TimeUtil.kt | 8 ++- .../java/com/tailscale/ipn/ui/view/Avatar.kt | 51 +++++++++++++++++ .../tailscale/ipn/ui/view/BugReportView.kt | 13 ++++- .../com/tailscale/ipn/ui/view/MainView.kt | 35 ++++++++---- .../com/tailscale/ipn/ui/view/PeerDetails.kt | 18 ++++-- .../com/tailscale/ipn/ui/view/SettingsView.kt | 43 ++++++++++++--- .../ipn/ui/viewModel/BugReportViewModel.kt | 8 +-- .../ipn/ui/viewModel/MainViewModel.kt | 55 ++++++++----------- android/src/main/res/values/strings.xml | 18 +++++- 12 files changed, 192 insertions(+), 71 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt diff --git a/android/build.gradle b/android/build.gradle index 181a0bc..be6cd06 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -92,6 +92,8 @@ dependencies { implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" implementation "androidx.navigation:navigation-compose:$nav_version" + // Supporting libraries. + implementation("io.coil-kt:coil-compose:1.3.1") // Tailscale dependencies. implementation ':ipn@aar' diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 3830681..f7094c4 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -74,6 +74,7 @@ class MainActivity : ComponentActivity() { ExitNodePicker(ExitNodePickerViewModel(manager.model)) } composable( + "peerDetails/{nodeId}", arguments = listOf(navArgument("nodeId") { type = NavType.StringType }) ) { @@ -120,7 +121,6 @@ class MainActivity : ComponentActivity() { startActivity(browserIntent) } - override fun onResume() { super.onResume() val restrictionsManager = diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt b/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt index de98904..b3be1de 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/PeerHelper.kt @@ -35,8 +35,8 @@ class PeerCategorizer(val model: IpnModel) { var selfPeers = (grouped[selfNode.User] ?: emptyList()).sortedBy { it.ComputedName } grouped.remove(selfNode.User) - val currentNode = selfPeers.first { it.ID == selfNode.ID } - currentNode.let { + val currentNode = selfPeers.firstOrNull { it.ID == selfNode.ID } + currentNode?.let { selfPeers = selfPeers.filter { it.ID != currentNode.ID } selfPeers = listOf(currentNode) + selfPeers } @@ -49,6 +49,10 @@ class PeerCategorizer(val model: IpnModel) { } val me = netmap.currentUserProfile() - return listOf(PeerSet(me, selfPeers)) + sorted + return if (selfPeers.isEmpty()) { + sorted + } else { + listOf(PeerSet(me, selfPeers)) + sorted + } } } \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt b/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt index 6e6150c..b999a36 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/TimeUtil.kt @@ -17,10 +17,13 @@ class TimeUtil { val diff = (expTime - now) / 1000 - if(diff < 0){ + if (diff < 0) { return "expired" } + // (jonathan) TODO: This is incorrect in a couple of ways + // - It needs to be in a composable so we can use stringResource + // - The string resources need to be proper plurals return when (diff) { in 0..60 -> "under a minute" in 61..3600 -> "in ${diff / 60} minutes" @@ -42,4 +45,5 @@ class TimeUtil { val i = Instant.from(ta) return Date.from(i) } -} \ No newline at end of file +} + diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt new file mode 100644 index 0000000..d018195 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt @@ -0,0 +1,51 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import coil.annotation.ExperimentalCoilApi +import coil.compose.rememberImagePainter +import com.tailscale.ipn.ui.model.IpnLocal + + +@OptIn(ExperimentalCoilApi::class) +@Composable +fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(size.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.tertiaryContainer) + ) { + profile?.UserProfile?.ProfilePicURL?.let { url -> + val painter = rememberImagePainter(data = url) + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.size(size.dp) + ) + } ?: run { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.size((size * .8f).dp) + ) + } + } +} \ No newline at end of file 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 478a51c..9a97ee2 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 @@ -8,6 +8,7 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width @@ -46,10 +47,11 @@ fun BugReportView(viewModel: BugReportViewModel) { val handler = LocalUriHandler.current Surface(color = MaterialTheme.colorScheme.surface) { - Column(modifier = defaultPaddingModifier().fillMaxWidth()) { + Column(modifier = defaultPaddingModifier().fillMaxWidth().fillMaxHeight()) { Text(text = stringResource(id = R.string.bug_report_title), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(8.dp)) @@ -70,6 +72,7 @@ fun BugReportView(viewModel: BugReportViewModel) { Text(text = stringResource(id = R.string.bug_report_id_desc), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Left, + color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.bodySmall) } } @@ -98,7 +101,9 @@ fun ReportIdRow(bugReportIdFlow: StateFlow) { @Composable fun contactText(): AnnotatedString { val annotatedString = buildAnnotatedString { - append(stringResource(id = R.string.bug_report_instructions_prefix)) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.bug_report_instructions_prefix)) + } pushStringAnnotation(tag = "reportLink", annotation = Links.SUPPORT_URL) withStyle(style = SpanStyle(color = Color.Blue)) { @@ -106,7 +111,9 @@ fun contactText(): AnnotatedString { } pop() - append(stringResource(id = R.string.bug_report_instructions_suffix)) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.bug_report_instructions_suffix)) + } } return annotatedString } 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 bbc3f54..98ea881 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 @@ -76,9 +76,11 @@ fun MainView(viewModel: MainViewModel, navigation: MainViewNavigation) { val isOn = viewModel.vpnToggleState.collectAsState(initial = false) Switch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value) - StateDisplay(viewModel.stateStr, viewModel.userName) - Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) { - SettingsButton(user.value, navigation.onNavigateToSettings) + StateDisplay(viewModel.stateRes, viewModel.userName) + Box(modifier = Modifier + .weight(1f) + .clickable { navigation.onNavigateToSettings() }, contentAlignment = Alignment.CenterEnd) { + Avatar(profile = user.value, size = 36) } } @@ -128,12 +130,13 @@ fun ExitNodeStatus(navAction: () -> Unit, exitNode: String = stringResource(id = } @Composable -fun StateDisplay(state: StateFlow, tailnet: String) { - val stateStr = state.collectAsState(initial = "--") +fun StateDisplay(state: StateFlow, tailnet: String) { + val stateVal = state.collectAsState(initial = R.string.placeholder) + val stateStr = stringResource(id = stateVal.value) Column(modifier = Modifier.padding(6.dp)) { - Text(text = "${tailnet}", style = MaterialTheme.typography.titleMedium) - Text(text = "${stateStr.value}", style = MaterialTheme.typography.bodyMedium) + Text(text = tailnet, style = MaterialTheme.typography.titleMedium) + Text(text = stateStr, style = MaterialTheme.typography.bodyMedium) } } @@ -158,10 +161,15 @@ fun StartingView() { modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.secondaryContainer), + .background(MaterialTheme.colorScheme.background), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally - ) { Text(text = stringResource(id = R.string.starting), style = MaterialTheme.typography.titleMedium) } + ) { + Text(text = stringResource(id = R.string.starting), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + } } @Composable @@ -170,15 +178,18 @@ fun ConnectView(user: IpnLocal.LoginProfile?, connectAction: () -> Unit, loginAc modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.secondaryContainer), + .background(MaterialTheme.colorScheme.background), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text(text = stringResource(id = R.string.not_connected), style = MaterialTheme.typography.titleMedium) + Text(text = stringResource(id = R.string.not_connected), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary) if (user != null && !user.isEmpty()) { val tailnetName = user.NetworkProfile?.DomainName ?: "" Text(stringResource(id = R.string.connect_to_tailnet, tailnetName), - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary ) Button(onClick = connectAction) { Text(text = stringResource(id = R.string.connect)) } } else { 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 9238457..21efb82 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 @@ -10,6 +10,7 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -36,24 +37,33 @@ import com.tailscale.ipn.ui.viewModel.PeerDetailsViewModel fun PeerDetails(viewModel: PeerDetailsViewModel) { Surface(color = MaterialTheme.colorScheme.surface) { - Column(modifier = Modifier.padding(horizontal = 8.dp)) { + Column(modifier = Modifier.padding(horizontal = 8.dp).fillMaxHeight()) { Column(modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = viewModel.nodeName, style = MaterialTheme.typography.titleMedium) + Text(text = viewModel.nodeName, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) Row(verticalAlignment = Alignment.CenterVertically) { Box(modifier = Modifier .size(8.dp) .background(color = viewModel.connectedColor, shape = RoundedCornerShape(percent = 50))) {} Spacer(modifier = Modifier.size(8.dp)) - Text(text = stringResource(id = viewModel.connectedStrRes), style = MaterialTheme.typography.bodyMedium) + Text(text = stringResource(id = viewModel.connectedStrRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) } } Spacer(modifier = Modifier.size(8.dp)) - Text(text = stringResource(id = R.string.addresses_section), style = MaterialTheme.typography.titleMedium) + Text(text = stringResource(id = R.string.addresses_section), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) Column(modifier = settingsRowModifier()) { viewModel.addresses.forEach { 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 35ba495..e4fce32 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 @@ -5,10 +5,13 @@ package com.tailscale.ipn.ui.view import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement 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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText @@ -30,9 +33,9 @@ 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.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links import com.tailscale.ipn.ui.model.IpnLocal @@ -55,7 +58,21 @@ fun Settings(viewModel: SettingsViewModel) { Surface(color = MaterialTheme.colorScheme.surface) { - Column(modifier = defaultPaddingModifier()) { + Column(modifier = defaultPaddingModifier().fillMaxHeight()) { + + Text(text = stringResource(id = R.string.settings_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium) + + Spacer(modifier = Modifier.height(8.dp)) + + // The login/logout button here is probably in the wrong location, but we need something + // somewhere for the time being. FUS should probably be implemented for V0 given that + // it's relatively simple to do so with localAPI. On iOS, the UI for user switching is + // all in the FUS screen. + viewModel.user?.let { user -> UserView(profile = user, viewModel.isAdmin, adminText(), onClick = { handler.openUri(Links.ADMIN_URL) @@ -70,7 +87,6 @@ fun Settings(viewModel: SettingsViewModel) { } } - Spacer(modifier = Modifier.height(8.dp)) viewModel.settings.forEach { settingBundle -> @@ -102,11 +118,18 @@ fun Settings(viewModel: SettingsViewModel) { @Composable fun UserView(profile: IpnLocal.LoginProfile?, isAdmin: Boolean, adminText: AnnotatedString, onClick: () -> Unit) { - Column(modifier = defaultPaddingModifier()) { - Column(modifier = settingsRowModifier().padding(8.dp)) { - Text(text = profile?.UserProfile?.DisplayName - ?: "", style = MaterialTheme.typography.titleMedium) - Text(text = profile?.Name ?: "", style = MaterialTheme.typography.bodyMedium) + 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) { @@ -150,7 +173,9 @@ fun SettingsSwitchRow(setting: Setting) { @Composable fun adminText(): AnnotatedString { val annotatedString = buildAnnotatedString { - append(stringResource(id = R.string.settings_admin_prefix)) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.settings_admin_prefix)) + } pushStringAnnotation(tag = "link", annotation = Links.ADMIN_URL) withStyle(style = SpanStyle(color = Color.Blue)) { diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/BugReportViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/BugReportViewModel.kt index 5d72334..9c958a9 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/BugReportViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/BugReportViewModel.kt @@ -7,21 +7,21 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tailscale.ipn.ui.localapi.LocalApiClient import com.tailscale.ipn.ui.model.BugReportID +import com.tailscale.ipn.ui.service.set import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class BugReportViewModel(localAPI: LocalApiClient) : ViewModel() { - private var _bugReportID: MutableStateFlow = MutableStateFlow("") - var bugReportID: StateFlow = _bugReportID + var bugReportID: StateFlow = MutableStateFlow("") init { viewModelScope.launch { localAPI.getBugReportId { when (it.successful) { - true -> _bugReportID.value = it.success ?: "(Error fetching ID)" - false -> _bugReportID.value = "(Error fetching ID)" + true -> bugReportID.set(it.success ?: "(Error fetching ID)") + false -> bugReportID.set("(Error fetching ID)") } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index 13e0ad6..eebee93 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -6,64 +6,57 @@ package com.tailscale.ipn.ui.viewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.Ipn.State import com.tailscale.ipn.ui.service.IpnActions import com.tailscale.ipn.ui.service.IpnModel +import com.tailscale.ipn.ui.service.set import com.tailscale.ipn.ui.util.PeerCategorizer import com.tailscale.ipn.ui.util.PeerSet import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class MainViewModel(val model: IpnModel, val actions: IpnActions) : ViewModel() { - private val _stateStr = MutableStateFlow("") - private val _tailnetName = MutableStateFlow("") - private val _vpnToggleState = MutableStateFlow(false) - private val _peers = MutableStateFlow>(emptyList()) - // The user readable state of the system - val stateStr = _stateStr.asStateFlow() - - // The current state of the IPN for determining view visibility - val ipnState = model.state - - // The name of the tailnet - val tailnetName = _tailnetName.asStateFlow() + val stateRes: StateFlow = MutableStateFlow(State.NoState.userStringRes()) // The expected state of the VPN toggle - val vpnToggleState = _vpnToggleState.asStateFlow() + val vpnToggleState: StateFlow = MutableStateFlow(false) // The list of peers - val peers = _peers.asStateFlow() + val peers: StateFlow> = MutableStateFlow(emptyList()) + + // The current state of the IPN for determining view visibility + val ipnState = model.state // The logged in user val loggedInUser = model.loggedInUser // The active search term for filtering peers - val searchTerm = MutableStateFlow("") + val searchTerm: StateFlow = MutableStateFlow("") init { viewModelScope.launch { model.state.collect { state -> - _stateStr.value = state.userString() - _vpnToggleState.value = (state == State.Running || state == State.Starting) + stateRes.set(state.userStringRes()) + vpnToggleState.set((state == State.Running || state == State.Starting)) } } viewModelScope.launch { model.netmap.collect { netmap -> - _tailnetName.value = netmap?.Domain ?: "" - _peers.value = PeerCategorizer(model).groupedAndFilteredPeers(searchTerm.value) + peers.set(PeerCategorizer(model).groupedAndFilteredPeers(searchTerm.value)) } } } fun searchPeers(searchTerm: String) { - this.searchTerm.value = searchTerm + this.searchTerm.set(searchTerm) viewModelScope.launch { - _peers.value = PeerCategorizer(model).groupedAndFilteredPeers(searchTerm) + peers.set(PeerCategorizer(model).groupedAndFilteredPeers(searchTerm)) } } @@ -85,15 +78,15 @@ class MainViewModel(val model: IpnModel, val actions: IpnActions) : ViewModel() } -private fun State?.userString(): String { +private fun State?.userStringRes(): Int { return when (this) { - State.NoState -> "Waiting..." - State.InUseOtherUser -> "--" - State.NeedsLogin -> "Please Login" - State.NeedsMachineAuth -> "--" - State.Stopped -> "Stopped" - State.Starting -> "Starting" - State.Running -> "Connected" - else -> "--" + State.NoState -> R.string.waiting + State.InUseOtherUser -> R.string.placeholder + State.NeedsLogin -> R.string.please_login + State.NeedsMachineAuth -> R.string.placeholder + State.Stopped -> R.string.stopped + State.Starting -> R.string.starting + State.Running -> R.string.connected + else -> R.string.placeholder } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 912f26e..316329b 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -30,14 +30,14 @@ Settings You can manage your account from the admin console.  - View admin console... + View admin console… About Bug Report Use Tailscale DNS Exit Node - Staring... + Starting… "Connect to your %1$s tailnet" @@ -49,5 +49,19 @@ Current MDM Settings MDM Settings + + Waiting… + -- + Please Login + Stopped + + + expired + under a minute + in %1$s minutes + in %1$s hours + in %1$s days + in %1$s months + in %1$s years