diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt index 827de6e..33eba9e 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt @@ -3,6 +3,15 @@ package com.tailscale.ipn.ui.model +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.tailscale.ipn.R +import com.tailscale.ipn.ui.theme.success +import com.tailscale.ipn.ui.util.ComposableStringFormatter +import com.tailscale.ipn.ui.util.DisplayAddress +import com.tailscale.ipn.ui.util.TimeUtil +import com.tailscale.ipn.ui.viewModel.PeerSettingInfo import kotlinx.serialization.Serializable class Tailcfg { @@ -82,6 +91,33 @@ class Tailcfg { val isMullvadNode: Boolean get() = Name.endsWith(".mullvad.ts.net.") + + val displayName: String + get() = ComputedName ?: "" + + fun connectedOrSelfNode(nm: Netmap.NetworkMap?) = + Online == true || StableID == nm?.SelfNode?.StableID + + fun connectedStrRes(nm: Netmap.NetworkMap?) = + if (connectedOrSelfNode(nm)) R.string.connected else R.string.not_connected + + @Composable + fun connectedColor(nm: Netmap.NetworkMap?) = + if (connectedOrSelfNode(nm)) MaterialTheme.colorScheme.success else Color.Gray + + val displayAddresses: List + get() { + var addresses = mutableListOf() + Addresses?.let { addresses.addAll(it.map { addr -> DisplayAddress(addr) }) } + addresses.add(DisplayAddress(Name)) + return addresses + } + + val info: List + get() = + listOf( + PeerSettingInfo(R.string.os, ComposableStringFormatter(Hostinfo.OS ?: "")), + PeerSettingInfo(R.string.key_expiry, TimeUtil().keyExpiryFromGoTime(KeyExpiry))) } @Serializable diff --git a/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt b/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt index 4128971..5ee4158 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/theme/Color.kt @@ -7,7 +7,5 @@ import androidx.compose.ui.graphics.Color // TODO: replace references to these with references to material theme val ts_color_light_blue = Color(0xFF4B70CC) -val ts_color_light_green = Color(0xFF1EA672) var ts_color_dark_desctrutive_text = Color(0xFFFF0000) -var ts_color_light_desctrutive_text = Color(0xFFBB0000) diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/FeatureStateRepresentation.kt b/android/src/main/java/com/tailscale/ipn/ui/util/FeatureStateRepresentation.kt index 8481dd9..c6d772f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/FeatureStateRepresentation.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/FeatureStateRepresentation.kt @@ -5,6 +5,7 @@ package com.tailscale.ipn.ui.util import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color // FeatureStateRepresentation represents the status of a feature @@ -12,7 +13,7 @@ import androidx.compose.ui.graphics.Color // It is typically implemented as an enumeration. interface FeatureStateRepresentation { @get:DrawableRes val symbolDrawable: Int - val tint: Color + @get:Composable val tint: Color @get:StringRes val title: Int @get:StringRes val caption: Int } diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt b/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt index bc05c93..f834a1c 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/Lists.kt @@ -13,7 +13,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp object Lists { @@ -27,7 +26,7 @@ object Lists { @Composable fun ItemDivider() { - HorizontalDivider(modifier = Modifier.alpha(0f)) + HorizontalDivider(color = MaterialTheme.colorScheme.outline) } } 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 4f04b3a..e401d12 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 @@ -55,15 +55,12 @@ import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.Permission import com.tailscale.ipn.ui.model.Permissions -import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.Tailcfg -import com.tailscale.ipn.ui.theme.ts_color_light_green import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.flag import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.viewModel.MainViewModel -import kotlinx.coroutines.flow.StateFlow // Navigation actions for the MainView data class MainViewNavigation( @@ -122,16 +119,12 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode PromptPermissionsIfNecessary(permissions = Permissions.all) - val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "") Row(modifier = Modifier.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, + viewModel = viewModel, onNavigateToPeerDetails = navigation.onNavigateToPeerDetails, onSearch = { viewModel.searchPeers(it) }) } @@ -279,16 +272,14 @@ fun ConnectView( @OptIn(ExperimentalMaterial3Api::class) @Composable fun PeerList( - searchTerm: StateFlow, - peers: StateFlow>, - state: StateFlow, - selfPeer: StableNodeID, + viewModel: MainViewModel, onNavigateToPeerDetails: (Tailcfg.Node) -> Unit, onSearch: (String) -> Unit ) { - val peerList = peers.collectAsState(initial = emptyList()) - val searchTermStr by searchTerm.collectAsState(initial = "") - val stateVal = state.collectAsState(initial = Ipn.State.NoState) + val peerList = viewModel.peers.collectAsState(initial = emptyList()) + val searchTermStr by viewModel.searchTerm.collectAsState(initial = "") + val stateVal = viewModel.ipnState.collectAsState(initial = Ipn.State.NoState) + val netmap = viewModel.netmap.collectAsState() SearchBar( query = searchTermStr, @@ -328,21 +319,12 @@ fun PeerList( Row(verticalAlignment = Alignment.CenterVertically) { // By definition, SelfPeer is online since we will not show the peer list // unless you're connected. - val isSelfAndRunning = - (peer.StableID == selfPeer && stateVal.value == Ipn.State.Running) - val color: Color = - if ((peer.Online == true) || isSelfAndRunning) { - ts_color_light_green - } else { - Color.Gray - } - Box(modifier = Modifier.padding(top = 3.dp)) { - Box( - modifier = - Modifier.size(10.dp) - .background( - color = color, shape = RoundedCornerShape(percent = 50))) {} - } + Box( + modifier = + Modifier.size(10.dp) + .background( + color = peer.connectedColor(netmap.value), + shape = RoundedCornerShape(percent = 50))) {} Spacer(modifier = Modifier.size(8.dp)) Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium) } 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 9554aea..bb76710 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 @@ -19,6 +19,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager @@ -50,32 +51,39 @@ fun PeerDetails( .padding(horizontal = 16.dp) .padding(top = 22.dp), ) { - Text(text = model.nodeName, style = MaterialTheme.typography.titleLarge) - 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) - } - Column(modifier = Modifier.fillMaxHeight()) { - Text( - text = stringResource(id = R.string.addresses_section), - style = MaterialTheme.typography.titleMedium) + model.netmap.collectAsState().value?.let { netmap -> + model.node.collectAsState().value?.let { node -> + Text(text = node.displayName, style = MaterialTheme.typography.titleLarge) + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.size(8.dp) + .background( + color = node.connectedColor(netmap), + shape = RoundedCornerShape(percent = 50))) {} + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(id = node.connectedStrRes(netmap)), + style = MaterialTheme.typography.bodyMedium) + } + Column(modifier = Modifier.fillMaxHeight()) { + Text( + text = stringResource(id = R.string.addresses_section), + style = MaterialTheme.typography.titleMedium) - Column(modifier = settingsRowModifier()) { - model.addresses.forEach { AddressRow(address = it.address, type = it.typeString) } - } + Column(modifier = settingsRowModifier()) { + node.displayAddresses.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()) { + node.info.forEach { + ValueRow(title = stringResource(id = it.titleRes), value = it.value.getString()) + } + } } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/PeerView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/PeerView.kt index bfa4131..13e1b5a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/PeerView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/PeerView.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Tailcfg -import com.tailscale.ipn.ui.theme.ts_color_light_green +import com.tailscale.ipn.ui.theme.success @Composable fun PeerView( @@ -43,7 +43,7 @@ fun PeerView( val isSelfAndRunning = (peer.StableID == selfPeer && stateVal == Ipn.State.Running) val color: Color = if ((peer.Online == true) || isSelfAndRunning) { - ts_color_light_green + MaterialTheme.colorScheme.success } else { Color.Gray } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt index 1da7513..403070c 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/DNSSettingsViewModel.kt @@ -5,6 +5,8 @@ package com.tailscale.ipn.ui.viewModel import android.util.Log import androidx.annotation.StringRes +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -14,8 +16,7 @@ import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.notifier.Notifier -import com.tailscale.ipn.ui.theme.ts_color_light_desctrutive_text -import com.tailscale.ipn.ui.theme.ts_color_light_green +import com.tailscale.ipn.ui.theme.success import com.tailscale.ipn.ui.util.FeatureStateRepresentation import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.set @@ -96,7 +97,7 @@ enum class DNSEnablementState : FeatureStateRepresentation { get() = R.string.tailscale_is_not_running_this_device_is_using_the_system_dns_resolver override val tint: Color - get() = Color.Gray + @Composable get() = Color.Gray override val symbolDrawable: Int get() = R.drawable.xmark_circle @@ -109,7 +110,7 @@ enum class DNSEnablementState : FeatureStateRepresentation { @StringRes get() = R.string.this_device_is_using_tailscale_to_resolve_dns_names override val tint: Color - get() = ts_color_light_green + @Composable get() = MaterialTheme.colorScheme.success override val symbolDrawable: Int get() = R.drawable.check_circle @@ -122,7 +123,7 @@ enum class DNSEnablementState : FeatureStateRepresentation { @StringRes get() = R.string.this_device_is_using_the_system_dns_resolver override val tint: Color - get() = ts_color_light_desctrutive_text + @Composable get() = MaterialTheme.colorScheme.success override val symbolDrawable: Int get() = R.drawable.xmark_circle 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 4434dcd..68f1cb4 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 @@ -8,7 +8,6 @@ import com.tailscale.ipn.R import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn.State -import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.PeerCategorizer @@ -38,9 +37,6 @@ class MainViewModel : IpnViewModel() { // The active search term for filtering peers val searchTerm: StateFlow = MutableStateFlow("") - // The peerID of the local node - val selfPeerId: StateFlow = MutableStateFlow("") - private val peerCategorizer = PeerCategorizer(viewModelScope) val userName: String @@ -59,7 +55,6 @@ class MainViewModel : IpnViewModel() { viewModelScope.launch { Notifier.netmap.collect { netmap -> peers.set(peerCategorizer.groupedAndFilteredPeers(searchTerm.value)) - selfPeerId.set(netmap?.SelfNode?.StableID ?: "") } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt index a41b05b..a1ef0d1 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/PeerDetailsViewModel.kt @@ -3,16 +3,18 @@ package com.tailscale.ipn.ui.viewModel -import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.tailscale.ipn.R +import androidx.lifecycle.viewModelScope +import com.tailscale.ipn.ui.model.Netmap import com.tailscale.ipn.ui.model.StableNodeID +import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.notifier.Notifier -import com.tailscale.ipn.ui.theme.ts_color_light_green import com.tailscale.ipn.ui.util.ComposableStringFormatter -import com.tailscale.ipn.ui.util.DisplayAddress -import com.tailscale.ipn.ui.util.TimeUtil +import com.tailscale.ipn.ui.util.set +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import java.io.File data class PeerSettingInfo(val titleRes: Int, val value: ComposableStringFormatter) @@ -25,29 +27,15 @@ class PeerDetailsViewModelFactory(private val nodeId: StableNodeID, private val } class PeerDetailsViewModel(val nodeId: StableNodeID, val filesDir: File) : IpnViewModel() { - - var addresses: List = emptyList() - var info: List = emptyList() - - val nodeName: String - val connectedStrRes: Int - val connectedColor: Color + val netmap: StateFlow = MutableStateFlow(null) + val node: StateFlow = MutableStateFlow(null) init { - val peer = Notifier.netmap.value?.getPeer(nodeId) - peer?.Addresses?.let { addresses = it.map { addr -> DisplayAddress(addr) } } - - peer?.Name?.let { addresses = listOf(DisplayAddress(it)) + addresses } - - peer?.let { p -> - info = - listOf( - PeerSettingInfo(R.string.os, ComposableStringFormatter(p.Hostinfo.OS ?: "")), - PeerSettingInfo(R.string.key_expiry, TimeUtil().keyExpiryFromGoTime(p.KeyExpiry))) + viewModelScope.launch { + Notifier.netmap.collect { nm -> + netmap.set(nm) + nm?.getPeer(nodeId)?.let { peer -> node.set(peer) } + } } - - nodeName = peer?.ComputedName ?: "" - connectedStrRes = if (peer?.Online == true) R.string.connected else R.string.not_connected - connectedColor = if (peer?.Online == true) ts_color_light_green else Color.Gray } }