android: make clipboard values clickable and focusable (#483)

also, use Column isntead of LazyColumn since the Tailnet lock view is a short list and doesn't require lazy rendering

Fixes tailscale/corp#21737

Signed-off-by: kari-ts <kari@tailscale.com>
pull/486/head^2
kari-ts 3 months ago committed by GitHub
parent d94125e767
commit b4ca226eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,6 +4,7 @@
package com.tailscale.ipn.ui.util
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@ -20,17 +21,15 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.theme.titledListItem
import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
@Composable
fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) {
val localClipboardManager = LocalClipboardManager.current
val modifier =
if (isAndroidTV()) {
Modifier
} else {
Modifier.clickable { localClipboardManager.setText(AnnotatedString(value)) }
}
Modifier.focusable()
.clickable {
localClipboardManager.setText(AnnotatedString(value))
}
ListItem(
colors = MaterialTheme.colorScheme.titledListItem,

@ -42,89 +42,96 @@ fun TailnetLockSetupView(
backToSettings: BackNavigation,
model: TailnetLockSetupViewModel = viewModel(factory = TailnetLockSetupViewModelFactory())
) {
val statusItems by model.statusItems.collectAsState()
val nodeKey by model.nodeKey.collectAsState()
val tailnetLockKey by model.tailnetLockKey.collectAsState()
val tailnetLockTlPubKey = tailnetLockKey.replace("nlpub", "tlpub")
Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding ->
LoadingIndicator.Wrap {
LazyColumn(modifier = Modifier.padding(innerPadding)) {
item(key = "header") { ExplainerView() }
items(items = statusItems, key = { "status_${it.title}" }) { statusItem ->
Lists.ItemDivider()
ListItem(
leadingContent = {
Icon(
painter = painterResource(id = statusItem.icon),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant)
},
headlineContent = { Text(stringResource(statusItem.title)) })
}
item(key = "nodeKey") {
Lists.SectionDivider()
ClipboardValueView(
value = nodeKey,
title = stringResource(R.string.node_key),
subtitle = stringResource(R.string.node_key_explainer))
}
item(key = "tailnetLockKey") {
Lists.SectionDivider()
ClipboardValueView(
value = tailnetLockTlPubKey,
title = stringResource(R.string.tailnet_lock_key),
subtitle = stringResource(R.string.tailnet_lock_key_explainer))
val statusItems by model.statusItems.collectAsState()
val nodeKey by model.nodeKey.collectAsState()
val tailnetLockKey by model.tailnetLockKey.collectAsState()
val tailnetLockTlPubKey = tailnetLockKey.replace("nlpub", "tlpub")
Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding ->
LoadingIndicator.Wrap {
Column(
modifier = Modifier
.padding(innerPadding)
.focusable()
.verticalScroll(rememberScrollState())
.fillMaxSize()
) {
ExplainerView()
statusItems.forEach { statusItem ->
Lists.ItemDivider()
ListItem(
leadingContent = {
Icon(
painter = painterResource(id = statusItem.icon),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
headlineContent = { Text(stringResource(statusItem.title)) }
)
}
//Node key
Lists.SectionDivider()
ClipboardValueView(
value = nodeKey,
title = stringResource(R.string.node_key),
subtitle = stringResource(R.string.node_key_explainer)
)
// Tailnet lock key
Lists.SectionDivider()
ClipboardValueView(
value = tailnetLockTlPubKey,
title = stringResource(R.string.tailnet_lock_key),
subtitle = stringResource(R.string.tailnet_lock_key_explainer)
)
}
}
}
}
}
}
@Composable
private fun ExplainerView() {
val handler = LocalUriHandler.current
Lists.MultilineDescription {
ClickableText(
explainerText(),
onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) },
style = MaterialTheme.typography.bodyMedium)
}
val handler = LocalUriHandler.current
Lists.MultilineDescription {
ClickableText(
explainerText(),
onClick = { handler.openUri(Links.TAILNET_LOCK_KB_URL) },
style = MaterialTheme.typography.bodyMedium
)
}
}
@Composable
fun explainerText(): AnnotatedString {
val annotatedString = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.defaultTextColor)) {
append(stringResource(id = R.string.tailnet_lock_explainer))
}
val annotatedString = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.defaultTextColor)) {
append(stringResource(id = R.string.tailnet_lock_explainer))
}
pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL)
pushStringAnnotation(tag = "tailnetLockSupportURL", annotation = Links.TAILNET_LOCK_KB_URL)
withStyle(
style =
SpanStyle(
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.link,
textDecoration = TextDecoration.Underline)) {
append(stringResource(id = R.string.learn_more))
textDecoration = TextDecoration.Underline
)
) {
append(stringResource(id = R.string.learn_more))
}
pop()
}
return annotatedString
pop()
}
return annotatedString
}
@Composable
@Preview
fun TailnetLockSetupViewPreview() {
val vm = TailnetLockSetupViewModel()
vm.nodeKey.set("8BADF00D-EA7-1337-DEAD-BEEF")
vm.tailnetLockKey.set("C0FFEE-CAFE-50DA")
TailnetLockSetupView(backToSettings = {}, vm)
val vm = TailnetLockSetupViewModel()
vm.nodeKey.set("8BADF00D-EA7-1337-DEAD-BEEF")
vm.tailnetLockKey.set("C0FFEE-CAFE-50DA")
TailnetLockSetupView(backToSettings = {}, vm)
}

Loading…
Cancel
Save