tailscale: Add device info and logout
This commit is contained in:
@@ -5,6 +5,7 @@ import android.net.NetworkCapabilities
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import android.provider.Settings
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -281,19 +282,20 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||||||
return RootShellSessionWrapper(rootSession)
|
return RootShellSessionWrapper(rootSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readSystemSSHHostKey(): io.nekohasekai.libbox.StringBox {
|
override fun readSystemSSHHostKey(): String {
|
||||||
error("not supported")
|
error("not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun lookupSFTPServer(): io.nekohasekai.libbox.StringBox {
|
override fun lookupSFTPServer(): String = runBlocking(Dispatchers.IO) {
|
||||||
val path = runBlocking(Dispatchers.IO) {
|
RootClient.lookupSFTPServer()
|
||||||
RootClient.lookupSFTPServer()
|
|
||||||
}
|
|
||||||
val result = io.nekohasekai.libbox.StringBox()
|
|
||||||
result.value = path
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun tailscaleHostname(): String = Settings.Global.getString(
|
||||||
|
Application.application.contentResolver,
|
||||||
|
Settings.Global.DEVICE_NAME,
|
||||||
|
)?.takeIf { it.isNotBlank() }
|
||||||
|
?: "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||||
|
|
||||||
override fun lookupUser(username: String?): io.nekohasekai.libbox.PlatformUser {
|
override fun lookupUser(username: String?): io.nekohasekai.libbox.PlatformUser {
|
||||||
val resolved = UserResolver.resolve(Application.packageManager, username!!)
|
val resolved = UserResolver.resolve(Application.packageManager, username!!)
|
||||||
val platformUser = io.nekohasekai.libbox.PlatformUser()
|
val platformUser = io.nekohasekai.libbox.PlatformUser()
|
||||||
|
|||||||
+91
-67
@@ -10,6 +10,8 @@ import androidx.compose.foundation.combinedClickable
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -23,8 +25,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||||
|
import androidx.compose.material.icons.filled.Computer
|
||||||
|
import androidx.compose.material.icons.filled.PowerSettingsNew
|
||||||
import androidx.compose.material.icons.filled.QrCode2
|
import androidx.compose.material.icons.filled.QrCode2
|
||||||
|
import androidx.compose.material.icons.filled.Router
|
||||||
import androidx.compose.material.icons.filled.Terminal
|
import androidx.compose.material.icons.filled.Terminal
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -98,8 +104,7 @@ fun TailscaleEndpointScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
val hasNetwork = endpoint.networkName.isNotEmpty()
|
val hasThisDevice = endpoint.backendState == "Running" && endpoint.selfPeer != null
|
||||||
val hasMagicDNS = endpoint.magicDNSSuffix.isNotEmpty()
|
|
||||||
val hasExitNode = endpoint.backendState == "Running" && endpoint.hasExitNodeCandidates
|
val hasExitNode = endpoint.backendState == "Running" && endpoint.hasExitNodeCandidates
|
||||||
val hasAuth = endpoint.authURL.isNotEmpty()
|
val hasAuth = endpoint.authURL.isNotEmpty()
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ fun TailscaleEndpointScreen(
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
val stateIsLast = !hasNetwork && !hasMagicDNS && !hasExitNode && !hasAuth
|
val stateIsLast = !hasThisDevice && !hasExitNode && !hasAuth
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
@@ -122,6 +127,13 @@ fun TailscaleEndpointScreen(
|
|||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.PowerSettingsNew,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -149,51 +161,51 @@ fun TailscaleEndpointScreen(
|
|||||||
),
|
),
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
if (hasNetwork) {
|
if (endpoint.backendState == "Running" && endpoint.selfPeer != null) {
|
||||||
val networkIsLast = !hasMagicDNS && !hasExitNode && !hasAuth
|
val thisDeviceIsLast = !hasExitNode && !hasAuth
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.tailscale_network),
|
stringResource(R.string.tailscale_this_device),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Computer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
endpoint.networkName,
|
endpoint.selfPeer.displayName,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = if (networkIsLast) {
|
trailingContent = {
|
||||||
Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
Icon(
|
||||||
} else {
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
Modifier
|
contentDescription = null,
|
||||||
},
|
modifier = Modifier.size(20.dp),
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
|
||||||
}
|
|
||||||
if (hasMagicDNS) {
|
|
||||||
val magicDNSIsLast = !hasExitNode && !hasAuth
|
|
||||||
ListItem(
|
|
||||||
headlineContent = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.tailscale_magic_dns),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
modifier = Modifier
|
||||||
Text(
|
.clip(
|
||||||
endpoint.magicDNSSuffix,
|
if (thisDeviceIsLast) {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
} else {
|
||||||
|
RoundedCornerShape(0.dp)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
.clickable {
|
||||||
modifier = if (magicDNSIsLast) {
|
navController.navigate(
|
||||||
Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
"tools/tailscale/${Uri.encode(endpointTag)}/peer/${Uri.encode(endpoint.selfPeer.id)}",
|
||||||
} else {
|
)
|
||||||
Modifier
|
},
|
||||||
},
|
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -206,14 +218,29 @@ fun TailscaleEndpointScreen(
|
|||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Router,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
endpoint.exitNode?.hostName ?: stringResource(R.string.disabled),
|
endpoint.exitNode?.displayName ?: stringResource(R.string.disabled),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(
|
.clip(
|
||||||
if (exitNodeIsLast) {
|
if (exitNodeIsLast) {
|
||||||
@@ -273,30 +300,6 @@ fun TailscaleEndpointScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This Device section
|
|
||||||
if (endpoint.backendState == "Running" && endpoint.selfPeer != null) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
SectionHeader(stringResource(R.string.tailscale_this_device))
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
PeerItem(
|
|
||||||
peer = endpoint.selfPeer,
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(
|
|
||||||
"tools/tailscale/${Uri.encode(endpointTag)}/peer/${Uri.encode(endpoint.selfPeer.id)}",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User group sections
|
// User group sections
|
||||||
for (group in endpoint.userGroups) {
|
for (group in endpoint.userGroups) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -388,7 +391,7 @@ private fun SectionHeader(title: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PeerItem(
|
private fun PeerItem(
|
||||||
peer: TailscalePeerData,
|
peer: TailscalePeerData,
|
||||||
@@ -407,20 +410,25 @@ private fun PeerItem(
|
|||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.Top,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.height(24.dp),
|
||||||
.size(8.dp)
|
contentAlignment = Alignment.Center,
|
||||||
.clip(CircleShape)
|
) {
|
||||||
.background(if (peer.online) Color(0xFF4CAF50) else Color.Gray),
|
Box(
|
||||||
)
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(if (peer.online) Color(0xFF4CAF50) else Color.Gray),
|
||||||
|
)
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
peer.hostName,
|
peer.displayName,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
if (firstIP != null) {
|
if (firstIP != null) {
|
||||||
@@ -431,13 +439,27 @@ private fun PeerItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (badges.isNotEmpty()) {
|
if (badges.isNotEmpty()) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
for (badge in badges) {
|
for (badge in badges) {
|
||||||
PeerBadgeView(badge)
|
PeerBadgeView(badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.height(24.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +467,7 @@ private data class PeerBadge(val text: String, val color: Color)
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun peerBadges(peer: TailscalePeerData): List<PeerBadge> {
|
private fun peerBadges(peer: TailscalePeerData): List<PeerBadge> {
|
||||||
|
if (!peer.online) return emptyList()
|
||||||
val badges = mutableListOf<PeerBadge>()
|
val badges = mutableListOf<PeerBadge>()
|
||||||
if (peer.shareeNode) {
|
if (peer.shareeNode) {
|
||||||
badges += PeerBadge(stringResource(R.string.tailscale_shared_in), Color(0xFFF44336))
|
badges += PeerBadge(stringResource(R.string.tailscale_shared_in), Color(0xFFF44336))
|
||||||
@@ -486,6 +509,7 @@ private fun PeerBadgeView(badge: PeerBadge) {
|
|||||||
text = badge.text,
|
text = badge.text,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(50))
|
.clip(RoundedCornerShape(50))
|
||||||
.background(badge.color)
|
.background(badge.color)
|
||||||
|
|||||||
+2
-2
@@ -80,7 +80,7 @@ fun TailscaleExitNodePickerScreen(
|
|||||||
val candidates = endpoint.userGroups
|
val candidates = endpoint.userGroups
|
||||||
.flatMap { it.peers }
|
.flatMap { it.peers }
|
||||||
.filter { it.exitNodeOption && it.stableID != selfStableID }
|
.filter { it.exitNodeOption && it.stableID != selfStableID }
|
||||||
.filter { searchText.isEmpty() || it.hostName.contains(searchText, ignoreCase = true) }
|
.filter { searchText.isEmpty() || it.displayName.contains(searchText, ignoreCase = true) || it.hostName.contains(searchText, ignoreCase = true) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -174,7 +174,7 @@ private fun PeerRow(
|
|||||||
)
|
)
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = peer.hostName,
|
text = peer.displayName,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
val firstIP = peer.tailscaleIPs.firstOrNull()
|
val firstIP = peer.tailscaleIPs.firstOrNull()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
@@ -75,7 +76,8 @@ fun TailscalePeerScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
val peer = viewModel.peer(endpointTag, peerId)
|
val peer = viewModel.peer(endpointTag, peerId)
|
||||||
val isSelf = viewModel.endpoint(endpointTag)?.selfPeer?.id == peerId
|
val endpoint = viewModel.endpoint(endpointTag)
|
||||||
|
val isSelf = endpoint?.selfPeer?.id == peerId
|
||||||
val pingViewModel: TailscalePingViewModel = viewModel()
|
val pingViewModel: TailscalePingViewModel = viewModel()
|
||||||
val pingState by pingViewModel.uiState.collectAsState()
|
val pingState by pingViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ fun TailscalePeerScreen(
|
|||||||
title = {
|
title = {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
peer?.hostName ?: "",
|
peer?.displayName ?: "",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
@@ -143,6 +145,60 @@ fun TailscalePeerScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
|
// Network section (self peer only): network name + logout
|
||||||
|
val networkName = endpoint?.networkName
|
||||||
|
val showLogout = isSelf && endpoint?.backendState == "Running" && endpoint?.keyAuth == false
|
||||||
|
if (isSelf && (!networkName.isNullOrEmpty() || showLogout)) {
|
||||||
|
SectionHeader(stringResource(R.string.tailscale_network))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
if (!networkName.isNullOrEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
AddressRow(
|
||||||
|
address = networkName,
|
||||||
|
label = stringResource(R.string.tailscale_network),
|
||||||
|
copied = copiedAddress,
|
||||||
|
onCopy = { copiedAddress = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showLogout) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.tailscale_logout),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
viewModel.logout(endpointTag)
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
// Tailscale Addresses section
|
// Tailscale Addresses section
|
||||||
SectionHeader(stringResource(R.string.tailscale_addresses))
|
SectionHeader(stringResource(R.string.tailscale_addresses))
|
||||||
Card(
|
Card(
|
||||||
@@ -165,6 +221,14 @@ fun TailscalePeerScreen(
|
|||||||
onCopy = { copiedAddress = it },
|
onCopy = { copiedAddress = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (peer.hostName.isNotEmpty()) {
|
||||||
|
AddressRow(
|
||||||
|
address = peer.hostName,
|
||||||
|
label = stringResource(R.string.tailscale_hostname),
|
||||||
|
copied = copiedAddress,
|
||||||
|
onCopy = { copiedAddress = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
for (ip in peer.tailscaleIPs) {
|
for (ip in peer.tailscaleIPs) {
|
||||||
AddressRow(
|
AddressRow(
|
||||||
address = ip,
|
address = ip,
|
||||||
|
|||||||
+15
-1
@@ -27,7 +27,9 @@ data class TailscalePeerData(
|
|||||||
val txBytes: Long,
|
val txBytes: Long,
|
||||||
val keyExpiry: Long,
|
val keyExpiry: Long,
|
||||||
val lastSeen: Long,
|
val lastSeen: Long,
|
||||||
)
|
) {
|
||||||
|
val displayName: String get() = dnsName.substringBefore(".").ifEmpty { hostName }
|
||||||
|
}
|
||||||
|
|
||||||
data class TailscaleUserGroupData(
|
data class TailscaleUserGroupData(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@@ -46,6 +48,7 @@ data class TailscaleEndpointData(
|
|||||||
val selfPeer: TailscalePeerData?,
|
val selfPeer: TailscalePeerData?,
|
||||||
val exitNode: TailscalePeerData?,
|
val exitNode: TailscalePeerData?,
|
||||||
val userGroups: List<TailscaleUserGroupData>,
|
val userGroups: List<TailscaleUserGroupData>,
|
||||||
|
val keyAuth: Boolean,
|
||||||
) {
|
) {
|
||||||
val hasExitNodeCandidates: Boolean
|
val hasExitNodeCandidates: Boolean
|
||||||
get() {
|
get() {
|
||||||
@@ -121,6 +124,16 @@ class TailscaleStatusViewModel : BaseViewModel<TailscaleStatusState, Nothing>()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logout(endpointTag: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Libbox.newStandaloneCommandClient().tailscaleLogout(endpointTag)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendErrorMessage(e.message ?: "logout failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun endpoint(tag: String): TailscaleEndpointData? = currentState.endpoints.firstOrNull { it.endpointTag == tag }
|
fun endpoint(tag: String): TailscaleEndpointData? = currentState.endpoints.firstOrNull { it.endpointTag == tag }
|
||||||
|
|
||||||
fun peer(endpointTag: String, peerId: String): TailscalePeerData? {
|
fun peer(endpointTag: String, peerId: String): TailscalePeerData? {
|
||||||
@@ -166,6 +179,7 @@ class TailscaleStatusViewModel : BaseViewModel<TailscaleStatusState, Nothing>()
|
|||||||
selfPeer = if (self != null) convertPeer(self) else null,
|
selfPeer = if (self != null) convertPeer(self) else null,
|
||||||
exitNode = if (exitNode != null) convertPeer(exitNode) else null,
|
exitNode = if (exitNode != null) convertPeer(exitNode) else null,
|
||||||
userGroups = userGroups,
|
userGroups = userGroups,
|
||||||
|
keyAuth = endpoint.keyAuth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -461,6 +461,7 @@
|
|||||||
<string name="tailscale_status">وضعیت</string>
|
<string name="tailscale_status">وضعیت</string>
|
||||||
<string name="tailscale_state">وضعیت</string>
|
<string name="tailscale_state">وضعیت</string>
|
||||||
<string name="tailscale_network">شبکه</string>
|
<string name="tailscale_network">شبکه</string>
|
||||||
|
<string name="tailscale_hostname">نام میزبان</string>
|
||||||
<string name="tailscale_open_auth_url">باز کردن لینک احراز هویت</string>
|
<string name="tailscale_open_auth_url">باز کردن لینک احراز هویت</string>
|
||||||
<string name="tailscale_open_auth_url_qr_code">نمایش QR کد لینک احراز هویت</string>
|
<string name="tailscale_open_auth_url_qr_code">نمایش QR کد لینک احراز هویت</string>
|
||||||
<string name="tailscale_this_device">این دستگاه</string>
|
<string name="tailscale_this_device">این دستگاه</string>
|
||||||
|
|||||||
@@ -467,6 +467,7 @@
|
|||||||
<string name="tailscale_status">Статус</string>
|
<string name="tailscale_status">Статус</string>
|
||||||
<string name="tailscale_state">Состояние</string>
|
<string name="tailscale_state">Состояние</string>
|
||||||
<string name="tailscale_network">Сеть</string>
|
<string name="tailscale_network">Сеть</string>
|
||||||
|
<string name="tailscale_hostname">Имя хоста</string>
|
||||||
<string name="tailscale_open_auth_url">Открыть URL авторизации</string>
|
<string name="tailscale_open_auth_url">Открыть URL авторизации</string>
|
||||||
<string name="tailscale_open_auth_url_qr_code">Показать QR-код авторизации</string>
|
<string name="tailscale_open_auth_url_qr_code">Показать QR-код авторизации</string>
|
||||||
<string name="tailscale_this_device">Это устройство</string>
|
<string name="tailscale_this_device">Это устройство</string>
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
<string name="tailscale_status">状态</string>
|
<string name="tailscale_status">状态</string>
|
||||||
<string name="tailscale_state">状态</string>
|
<string name="tailscale_state">状态</string>
|
||||||
<string name="tailscale_network">网络</string>
|
<string name="tailscale_network">网络</string>
|
||||||
|
<string name="tailscale_hostname">主机名</string>
|
||||||
<string name="tailscale_open_auth_url">打开认证链接</string>
|
<string name="tailscale_open_auth_url">打开认证链接</string>
|
||||||
<string name="tailscale_open_auth_url_qr_code">显示认证链接二维码</string>
|
<string name="tailscale_open_auth_url_qr_code">显示认证链接二维码</string>
|
||||||
<string name="tailscale_this_device">此设备</string>
|
<string name="tailscale_this_device">此设备</string>
|
||||||
|
|||||||
@@ -461,6 +461,7 @@
|
|||||||
<string name="tailscale_status">狀態</string>
|
<string name="tailscale_status">狀態</string>
|
||||||
<string name="tailscale_state">狀態</string>
|
<string name="tailscale_state">狀態</string>
|
||||||
<string name="tailscale_network">網路</string>
|
<string name="tailscale_network">網路</string>
|
||||||
|
<string name="tailscale_hostname">主機名</string>
|
||||||
<string name="tailscale_open_auth_url">開啟認證連結</string>
|
<string name="tailscale_open_auth_url">開啟認證連結</string>
|
||||||
<string name="tailscale_open_auth_url_qr_code">顯示認證連結 QR 碼</string>
|
<string name="tailscale_open_auth_url_qr_code">顯示認證連結 QR 碼</string>
|
||||||
<string name="tailscale_this_device">此裝置</string>
|
<string name="tailscale_this_device">此裝置</string>
|
||||||
|
|||||||
@@ -463,9 +463,11 @@
|
|||||||
<string name="tailscale_state">State</string>
|
<string name="tailscale_state">State</string>
|
||||||
<string name="tailscale_network">Network</string>
|
<string name="tailscale_network">Network</string>
|
||||||
<string name="tailscale_magic_dns" translatable="false">MagicDNS</string>
|
<string name="tailscale_magic_dns" translatable="false">MagicDNS</string>
|
||||||
|
<string name="tailscale_hostname">Hostname</string>
|
||||||
<string name="tailscale_open_auth_url">Open Auth URL</string>
|
<string name="tailscale_open_auth_url">Open Auth URL</string>
|
||||||
<string name="tailscale_open_auth_url_qr_code">Show Auth URL QR Code</string>
|
<string name="tailscale_open_auth_url_qr_code">Show Auth URL QR Code</string>
|
||||||
<string name="tailscale_this_device">This Device</string>
|
<string name="tailscale_this_device">This Device</string>
|
||||||
|
<string name="tailscale_logout">Log out</string>
|
||||||
<string name="tailscale_connected">Connected</string>
|
<string name="tailscale_connected">Connected</string>
|
||||||
<string name="tailscale_not_connected">Not Connected</string>
|
<string name="tailscale_not_connected">Not Connected</string>
|
||||||
<string name="tailscale_addresses">Tailscale Addresses</string>
|
<string name="tailscale_addresses">Tailscale Addresses</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user