mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 00:37:41 +00:00
feat(amethyst): redesign the Nostr Signer screen around a scannable hero
Rework the signer settings screen from a plain toggle list into a purpose- built pairing surface: - A disabled-state hero (key medallion, headline, one big "Turn on signer" call to action) that reads as a feature intro rather than a setting. - A live status card with an animated pulsing dot summarising "signing for N apps · listening on M relays". - A QR hero: the bunker:// address rendered as a large scannable QR (QrCodeDrawer) on a white surface — pairing is scan-first, with copy and new-secret as tonal actions and the raw string kept as a caption. - Scan-to-connect: a primary "Scan a code" button opening the QR scanner for nostrconnect:// offers, with paste-a-link as a revealable fallback. - A connected-apps row showing the live count and linking into Connected Apps. Reuses the existing QrCodeDrawer / SimpleQrCodeScanner composables. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FHr2mu5SiHwYNR7evYUuF
This commit is contained in:
+337
-114
@@ -22,54 +22,80 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.napplet.signers.Nip46PermissionAuthorizer
|
||||
import com.vitorpamplona.amethyst.model.nip46Signer.Nip46SignerState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val LiveGreen = Color(0xFF3DDC84)
|
||||
|
||||
@Composable
|
||||
fun Nip46SignerScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -86,11 +112,36 @@ fun Nip46SignerScreen(
|
||||
val relays by signer.listeningRelays.collectAsStateWithLifecycle()
|
||||
val writeable = remember { account.signer.isWriteable() }
|
||||
|
||||
var connectedCount by remember { mutableIntStateOf(0) }
|
||||
var refreshKey by remember { mutableIntStateOf(0) }
|
||||
var scanning by remember { mutableStateOf(false) }
|
||||
|
||||
var bunkerUri by remember { mutableStateOf<String?>(null) }
|
||||
// Recompute the advertised bunker URI whenever it is being shown or its inputs change.
|
||||
LaunchedEffect(enabled, secret, relays) {
|
||||
bunkerUri = if (enabled && writeable && relays.isNotEmpty()) signer.bunkerUri() else null
|
||||
}
|
||||
LaunchedEffect(enabled, refreshKey) {
|
||||
connectedCount =
|
||||
account.signerPermissionLedger.store
|
||||
.allPolicies()
|
||||
.keys
|
||||
.count { it.startsWith("${Nip46PermissionAuthorizer.COORDINATE_PREFIX}:") }
|
||||
}
|
||||
|
||||
fun onConnect(uri: String) {
|
||||
scope.launch {
|
||||
val result = signer.connectViaNostrConnect(uri.trim())
|
||||
Toast.makeText(context, describe(context, result), Toast.LENGTH_LONG).show()
|
||||
refreshKey++
|
||||
}
|
||||
}
|
||||
|
||||
if (scanning) {
|
||||
SimpleQrCodeScanner { contents ->
|
||||
scanning = false
|
||||
contents?.let { onConnect(it) }
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopBarWithBackButton(stringResource(R.string.nip46_signer_title), nav) },
|
||||
@@ -101,157 +152,219 @@ fun Nip46SignerScreen(
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||
) {
|
||||
HeaderCard()
|
||||
|
||||
if (!writeable) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_readonly),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
ReadOnlyNotice()
|
||||
return@Column
|
||||
}
|
||||
|
||||
EnableRow(enabled = enabled, onToggle = { signer.setEnabled(it) })
|
||||
if (!enabled) {
|
||||
DisabledHero(onEnable = { signer.setEnabled(true) })
|
||||
return@Column
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (relays.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_status_no_relays),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
pluralStringResource(R.plurals.nip46_signer_status_listening, relays.size, relays.size),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
LiveStatusCard(
|
||||
relayCount = relays.size,
|
||||
connectedCount = connectedCount,
|
||||
onToggleOff = { signer.setEnabled(false) },
|
||||
)
|
||||
|
||||
bunkerUri?.let { uri ->
|
||||
BunkerAddressCard(
|
||||
uri = uri,
|
||||
onCopy = {
|
||||
clipboard.setText(AnnotatedString(uri))
|
||||
Toast.makeText(context, R.string.nip46_signer_copied, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onRegenerate = {
|
||||
signer.regenerateSecret()
|
||||
Toast.makeText(context, R.string.nip46_signer_regenerated, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
)
|
||||
}
|
||||
if (relays.isEmpty()) {
|
||||
WarningCard(stringResource(R.string.nip46_signer_status_no_relays))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ConnectAppSection(
|
||||
onConnect = { uri ->
|
||||
scope.launch {
|
||||
val result = signer.connectViaNostrConnect(uri.trim())
|
||||
Toast.makeText(context, describe(context, result), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
bunkerUri?.let { uri ->
|
||||
QrHeroCard(
|
||||
uri = uri,
|
||||
onCopy = {
|
||||
clipboard.setText(AnnotatedString(uri))
|
||||
Toast.makeText(context, R.string.nip46_signer_copied, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onRegenerate = {
|
||||
signer.regenerateSecret()
|
||||
Toast.makeText(context, R.string.nip46_signer_regenerated, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
)
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { nav.nav(Route.ConnectedApps) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.nip46_signer_manage_apps))
|
||||
}
|
||||
}
|
||||
|
||||
ConnectSection(
|
||||
onScan = { scanning = true },
|
||||
onPaste = { onConnect(it) },
|
||||
)
|
||||
|
||||
ConnectedAppsRow(
|
||||
count = connectedCount,
|
||||
onClick = { nav.nav(Route.ConnectedApps) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
private fun HeaderCard() {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
private fun DisabledHero(onEnable: () -> Unit) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(104.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Key,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(36.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(52.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_hero_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Button(
|
||||
onClick = onEnable,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Icon(MaterialSymbols.Key, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.nip46_signer_turn_on), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnableRow(
|
||||
enabled: Boolean,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
private fun LiveStatusCard(
|
||||
relayCount: Int,
|
||||
connectedCount: Int,
|
||||
onToggleOff: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_enable),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(checked = enabled, onCheckedChange = onToggle)
|
||||
Row(
|
||||
modifier = Modifier.padding(18.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
LiveDot()
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_live),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
Text(
|
||||
buildString {
|
||||
append(pluralStringResource(R.plurals.nip46_signer_connected_count, connectedCount, connectedCount))
|
||||
append(" · ")
|
||||
append(pluralStringResource(R.plurals.nip46_signer_status_listening, relayCount, relayCount))
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||
)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = { onToggleOff() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BunkerAddressCard(
|
||||
private fun LiveDot() {
|
||||
val transition = rememberInfiniteTransition(label = "live")
|
||||
val alpha by transition.animateFloat(
|
||||
initialValue = 0.25f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(tween(900), RepeatMode.Reverse),
|
||||
label = "pulse",
|
||||
)
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(18.dp)) {
|
||||
Canvas(Modifier.size(18.dp)) { drawCircle(color = LiveGreen, alpha = alpha * 0.35f) }
|
||||
Canvas(Modifier.size(9.dp)) { drawCircle(color = LiveGreen) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QrHeroCard(
|
||||
uri: String,
|
||||
onCopy: () -> Unit,
|
||||
onRegenerate: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color.White,
|
||||
) {
|
||||
QrCodeDrawer(
|
||||
contents = uri,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
.size(232.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_bunker_uri_label),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
stringResource(R.string.nip46_signer_scan_caption),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
uri,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 3,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_bunker_uri_hint),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Button(onClick = onCopy, modifier = Modifier.weight(1f)) {
|
||||
FilledTonalButton(onClick = onCopy, modifier = Modifier.weight(1f)) {
|
||||
Icon(MaterialSymbols.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.nip46_signer_copy))
|
||||
}
|
||||
FilledTonalButton(onClick = onRegenerate, modifier = Modifier.weight(1f)) {
|
||||
Icon(MaterialSymbols.Refresh, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.nip46_signer_regenerate))
|
||||
}
|
||||
}
|
||||
@@ -260,31 +373,141 @@ private fun BunkerAddressCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectAppSection(onConnect: (String) -> Unit) {
|
||||
private fun ConnectSection(
|
||||
onScan: () -> Unit,
|
||||
onPaste: (String) -> Unit,
|
||||
) {
|
||||
var showPaste by remember { mutableStateOf(false) }
|
||||
var input by remember { mutableStateOf("") }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_connect_label),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
placeholder = { Text(stringResource(R.string.nip46_signer_connect_hint)) },
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (input.isNotBlank()) {
|
||||
onConnect(input)
|
||||
input = ""
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = input.isNotBlank(),
|
||||
onClick = onScan,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(54.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.nip46_signer_connect_button))
|
||||
Icon(MaterialSymbols.CameraAlt, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.nip46_signer_scan_connect), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
TextButton(onClick = { showPaste = !showPaste }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.nip46_signer_paste_link))
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = showPaste) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
OutlinedTextField(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
placeholder = { Text(stringResource(R.string.nip46_signer_connect_hint)) },
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (input.isNotBlank()) {
|
||||
onPaste(input)
|
||||
input = ""
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = input.isNotBlank(),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||
) {
|
||||
Text(stringResource(R.string.nip46_signer_connect_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectedAppsRow(
|
||||
count: Int,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Apps,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_manage_apps),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
pluralStringResource(R.plurals.nip46_signer_connected_count, count, count),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
MaterialSymbols.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningCard(message: String) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
|
||||
) {
|
||||
Text(
|
||||
message,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReadOnlyNotice() {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 24.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(18.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(MaterialSymbols.Key, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(28.dp))
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_readonly),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,6 +926,16 @@
|
||||
<string name="nip46_signer_connect_failed">Could not connect: %1$s</string>
|
||||
<string name="nip46_signer_readonly">This is a read-only account and cannot sign.</string>
|
||||
<string name="nip46_signer_remote_app">Remote signer app</string>
|
||||
<string name="nip46_signer_hero_title">Sign for other apps</string>
|
||||
<string name="nip46_signer_turn_on">Turn on signer</string>
|
||||
<string name="nip46_signer_live">Live</string>
|
||||
<string name="nip46_signer_scan_caption">Scan to connect an app to your key</string>
|
||||
<string name="nip46_signer_scan_connect">Scan a code</string>
|
||||
<string name="nip46_signer_paste_link">Paste a link instead</string>
|
||||
<plurals name="nip46_signer_connected_count">
|
||||
<item quantity="one">%1$d connected app</item>
|
||||
<item quantity="other">%1$d connected apps</item>
|
||||
</plurals>
|
||||
<string name="napplet_connected_app_trust_level">Signing trust level</string>
|
||||
<string name="napplet_connected_app_capabilities">Capabilities</string>
|
||||
<string name="napplet_connected_app_forget">Forget this app</string>
|
||||
|
||||
Reference in New Issue
Block a user