Improve backup screen interface with better visual design

- Replace flat layout with Material3 ElevatedCards for tips, password, and per-account sections
- Add collapsible tips section (collapsed by default) to reduce initial clutter
- Show backup status badge (green checkmark / red warning) per account
- Display warning message for accounts that haven't been backed up yet
- Place Copy and QR buttons side by side in a row to save vertical space
- Add icons to all action buttons (ContentCopy, QrCode2, Lock)
- Improve password section with markdown-rendered encryption info
- Extract sub-composables (TipsCard, PasswordCard, AccountBackupCard, BackupIconButton) for clarity
- Add onBackupDone callback to update per-account status reactively

https://claude.ai/code/session_016hcA2xfnBikdiGctfcAxZZ
This commit is contained in:
Claude
2026-03-06 20:49:57 +00:00
parent a31314e699
commit a9bf7769a0
2 changed files with 414 additions and 276 deletions
@@ -8,6 +8,7 @@ import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -18,16 +19,30 @@ 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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.ExpandLess
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.QrCode2
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -35,6 +50,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -67,6 +83,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -83,7 +100,6 @@ import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator
import com.greenart7c3.nostrsigner.ui.InnerQrCodeDrawer
import com.greenart7c3.nostrsigner.ui.QrCodeDrawer
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.CloseButton
import com.greenart7c3.nostrsigner.ui.components.SeedWordsPage
import com.greenart7c3.nostrsigner.ui.navigation.Route
@@ -108,206 +124,323 @@ fun AccountBackupScreen(
var isLoading by remember { mutableStateOf(false) }
Surface(
modifier =
modifier
.fillMaxSize(),
modifier = modifier.fillMaxSize(),
) {
Column(
modifier =
Modifier
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (isLoading) {
CenterCircularProgressIndicator(Modifier, text = stringResource(R.string.do_not_leave_the_app_until_the_key_is_generated))
} else {
val content = stringResource(R.string.account_backup_tips_md)
if (isLoading) {
CenterCircularProgressIndicator(Modifier, text = stringResource(R.string.do_not_leave_the_app_until_the_key_is_generated))
} else {
// Collapsible tips section
var tipsExpanded by remember { mutableStateOf(false) }
TipsCard(expanded = tipsExpanded, onToggle = { tipsExpanded = !tipsExpanded })
val astNode =
remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
Spacer(modifier = Modifier.height(16.dp))
// Password section
val password = remember { mutableStateOf(TextFieldValue("")) }
var errorMessage by remember { mutableStateOf("") }
var showCharsPassword by remember { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
PasswordCard(
password = password.value,
errorMessage = errorMessage,
showCharsPassword = showCharsPassword,
onPasswordChange = {
password.value = it
if (errorMessage.isNotEmpty()) errorMessage = ""
},
onToggleVisibility = { showCharsPassword = !showCharsPassword },
onDone = {
if (password.value.text.isBlank()) {
errorMessage = Amber.instance.getString(R.string.password_is_required)
}
keyboardController?.hide()
},
)
Spacer(modifier = Modifier.height(16.dp))
LocalPreferences.allSavedAccounts(Amber.instance).forEach { accountInfo ->
var localAccount by remember { mutableStateOf<Account?>(null) }
var seedWords by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
localAccount = LocalPreferences.loadFromEncryptedStorage(Amber.instance, accountInfo.npub)
localAccount?.let { acc ->
seedWords = acc.seedWords()
}
}
}
localAccount?.let { account ->
AccountBackupCard(
account = account,
password = password.value.text,
seedWords = seedWords,
onLoading = { isLoading = it },
navController = navController,
)
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
}
}
@Composable
private fun TipsCard(expanded: Boolean, onToggle: () -> Unit) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Outlined.Lock,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.account_backup),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
}
TextButton(onClick = onToggle) {
Text(if (expanded) stringResource(R.string.hide) else stringResource(R.string.show_tips))
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = if (expanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
}
AnimatedVisibility(visible = expanded) {
Column {
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
val content = stringResource(R.string.account_backup_tips_md)
val astNode = remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
style = RichTextStyle().resolveDefaults(),
modifier = Modifier.padding(top = 16.dp),
modifier = Modifier.padding(bottom = 4.dp),
) {
BasicMarkdown(astNode)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(30.dp))
val content1 = stringResource(R.string.account_backup_tips3_md_new)
val astNode1 =
remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
style = RichTextStyle().resolveDefaults(),
) {
BasicMarkdown(astNode1)
}
Spacer(modifier = Modifier.height(10.dp))
val password = remember { mutableStateOf(TextFieldValue("")) }
var errorMessage by remember { mutableStateOf("") }
var showCharsPassword by remember { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.semantics {
contentType = ContentType.Password
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PasswordCard(
password: TextFieldValue,
errorMessage: String,
showCharsPassword: Boolean,
onPasswordChange: (TextFieldValue) -> Unit,
onToggleVisibility: () -> Unit,
onDone: () -> Unit,
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Outlined.Lock,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.encryption_password),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
)
}
Spacer(modifier = Modifier.height(4.dp))
val tipsContent = stringResource(R.string.account_backup_tips3_md)
val tipsAstNode = remember {
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(tipsContent)
}
RichText(
style = RichTextStyle().resolveDefaults(),
modifier = Modifier.fillMaxWidth(),
) {
BasicMarkdown(tipsAstNode)
}
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.semantics { contentType = ContentType.Password },
value = password,
onValueChange = onPasswordChange,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { onDone() }),
placeholder = { Text(text = stringResource(R.string.encryption_password)) },
trailingIcon = {
IconButton(onClick = onToggleVisibility) {
Icon(
imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
contentDescription = if (showCharsPassword) {
stringResource(R.string.show_password)
} else {
stringResource(R.string.hide_password)
},
value = password.value,
onValueChange = {
password.value = it
if (errorMessage.isNotEmpty()) {
errorMessage = ""
}
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
if (password.value.text.isBlank()) {
errorMessage = Amber.instance.getString(R.string.password_is_required)
}
keyboardController?.hide()
},
),
placeholder = {
Text(
text = stringResource(R.string.encryption_password),
)
},
trailingIcon = {
Row {
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
Icon(
imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
contentDescription = if (showCharsPassword) {
stringResource(R.string.show_password)
} else {
stringResource(
R.string.hide_password,
)
},
)
}
}
},
visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
)
if (errorMessage.isNotBlank()) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
)
}
},
visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
isError = errorMessage.isNotBlank(),
supportingText = if (errorMessage.isNotBlank()) {
{ Text(text = errorMessage, color = MaterialTheme.colorScheme.error) }
} else {
null
},
singleLine = true,
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AccountBackupCard(
account: Account,
password: String,
seedWords: String,
onLoading: (Boolean) -> Unit,
navController: NavHostController,
) {
val profileUrl = account.picture.collectAsState()
val name = account.name.collectAsState()
var didBackup by remember { mutableStateOf(account.didBackup) }
LocalPreferences.allSavedAccounts(Amber.instance).forEach {
var localAccount by remember { mutableStateOf<Account?>(null) }
var seedWords by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
localAccount = LocalPreferences.loadFromEncryptedStorage(Amber.instance, it.npub)
localAccount?.let { acc ->
seedWords = acc.seedWords()
}
}
}
localAccount?.let { localAccount ->
Column(
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
// Account header row
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
if (profileUrl.value.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) {
SubcomposeAsyncImage(
model = profileUrl.value,
contentDescription = "profile picture",
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
loading = { CenterCircularProgressIndicator(Modifier) },
error = {
Icon(
Icons.Outlined.Person,
contentDescription = "profile picture",
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.border(
2.dp,
Color.LightGray,
RoundedCornerShape(8.dp),
)
.padding(8.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
) {
val profileUrl = localAccount.picture.collectAsState()
val name = localAccount.name.collectAsState()
if (profileUrl.value.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) {
SubcomposeAsyncImage(
profileUrl,
"profile picture",
Modifier
.clip(
RoundedCornerShape(50),
)
.height(40.dp)
.width(40.dp),
loading = {
CenterCircularProgressIndicator(Modifier)
},
error = { _ ->
Icon(
Icons.Outlined.Person,
"profile picture",
modifier = Modifier.border(
2.dp,
Color.fromHex(localAccount.hexKey.slice(0..5)),
CircleShape,
),
)
},
)
} else {
Icon(
Icons.Outlined.Person,
"profile picture",
modifier = Modifier
.height(40.dp)
.width(40.dp)
.border(
2.dp,
Color.fromHex(localAccount.hexKey.slice(0..5)),
CircleShape,
),
)
}
Text(
modifier = Modifier
.padding(start = 4.dp),
text = name.value.ifBlank { it.npub.toShortenHex() },
fontWeight = FontWeight.Bold,
)
}
NSecCopyButton(localAccount, password.value.text, onLoading = { value -> isLoading = value })
NSecQrButton(localAccount, password.value.text, onLoading = { value -> isLoading = value }, navController = navController)
if (Nip06().isValidMnemonic(seedWords)) {
SeedWordsButton(localAccount, seedWords)
}
}
}
}
.size(48.dp)
.border(2.dp, Color.fromHex(account.hexKey.slice(0..5)), CircleShape),
)
},
)
} else {
Icon(
Icons.Outlined.Person,
contentDescription = "profile picture",
modifier = Modifier
.size(48.dp)
.border(2.dp, Color.fromHex(account.hexKey.slice(0..5)), CircleShape)
.clip(CircleShape),
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = name.value.ifBlank { account.hexKey.toShortenHex() },
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
}
// Backup status badge
if (didBackup) {
Icon(
imageVector = Icons.Outlined.CheckCircle,
contentDescription = "Backed up",
tint = Color(0xFF4CAF50),
modifier = Modifier.size(24.dp),
)
} else {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = "Not backed up",
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp),
)
}
}
if (!didBackup) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.make_backup_message),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
// Copy and QR buttons side by side
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
NSecCopyButton(
account = account,
password = password,
modifier = Modifier.weight(1f),
onLoading = onLoading,
onBackupDone = { didBackup = true },
)
NSecQrButton(
account = account,
password = password,
modifier = Modifier.weight(1f),
onLoading = onLoading,
onBackupDone = { didBackup = true },
navController = navController,
)
}
// Seed words button (full width, shown only if valid mnemonic)
if (Nip06().isValidMnemonic(seedWords)) {
Spacer(modifier = Modifier.height(4.dp))
SeedWordsButton(
account = account,
seedWords = seedWords,
onBackupDone = { didBackup = true },
)
}
}
}
@@ -317,58 +450,58 @@ fun AccountBackupScreen(
private fun NSecQrButton(
account: Account,
password: String,
modifier: Modifier = Modifier,
onLoading: (Boolean) -> Unit,
onBackupDone: () -> Unit,
navController: NavHostController,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val showDialog = remember {
mutableStateOf(false)
}
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) {
var nsec by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
nsec = account.getNsec()
}
}
if (nsec.isNotBlank()) {
QrCodeDialog(
content = nsec,
) {
QrCodeDialog(content = nsec) {
Amber.instance.applicationIOScope.launch {
account.didBackup = true
onBackupDone()
showDialog.value = false
}
}
}
}
val keyguardLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
if (password.isNotBlank()) {
Amber.instance.applicationIOScope.launch {
onLoading(true)
val ncryptsec = account.nip49Encrypt(password)
account.didBackup = true
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec))
}
onLoading(false)
val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
if (password.isNotBlank()) {
Amber.instance.applicationIOScope.launch {
onLoading(true)
val ncryptsec = account.nip49Encrypt(password)
account.didBackup = true
onBackupDone()
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec))
}
} else {
showDialog.value = true
onLoading(false)
}
} else {
showDialog.value = true
}
}
}
val text = stringResource(if (password.isBlank()) R.string.show_qr_code else R.string.encrypt_and_show_qr_code)
AmberButton(
BackupIconButton(
modifier = modifier,
icon = { Icon(Icons.Outlined.QrCode2, contentDescription = null, modifier = Modifier.size(18.dp)) },
text = text,
onClick = {
authenticate(
title = text,
@@ -380,6 +513,7 @@ private fun NSecQrButton(
onLoading(true)
val ncryptsec = account.nip49Encrypt(password)
account.didBackup = true
onBackupDone()
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec))
}
@@ -391,17 +525,11 @@ private fun NSecQrButton(
},
onError = { _, message ->
scope.launch {
Toast.makeText(
context,
message,
Toast.LENGTH_SHORT,
).show()
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
},
)
},
text = text,
)
}
@@ -411,12 +539,10 @@ fun QrCodeScreen(
content: String,
) {
Column(
modifier = modifier
.fillMaxSize(),
modifier = modifier.fillMaxSize(),
) {
Column(
modifier = Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
@@ -446,7 +572,6 @@ fun QrCodeDialog(
window?.attributes = window.attributes?.apply {
screenBrightness = 1f
}
onDispose {
window?.attributes = window.attributes?.apply {
screenBrightness = originalBrightness ?: WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
@@ -473,7 +598,6 @@ fun QrCodeDialog(
) {
CloseButton(onCancel = onClose)
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -491,34 +615,28 @@ fun QrCodeDialog(
private fun SeedWordsButton(
account: Account,
seedWords: String,
onBackupDone: () -> Unit,
) {
val showSeedWords = remember { mutableStateOf(false) }
val context = LocalContext.current
val keyguardLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
showSeedWords.value = true
}
val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
showSeedWords.value = true
}
}
if (showSeedWords.value) {
ModalBottomSheet(
onDismissRequest = {
showSeedWords.value = false
},
onDismissRequest = { showSeedWords.value = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
) {
Column(
modifier =
Modifier
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize(),
) {
CompositionLocalProvider(
LocalDensity provides Density(
LocalDensity.current.density,
1f,
),
LocalDensity provides Density(LocalDensity.current.density, 1f),
) {
SeedWordsPage(
seedWords = seedWords.split(" ").toSet(),
@@ -531,7 +649,11 @@ private fun SeedWordsButton(
val text = stringResource(R.string.show_seed_words)
AmberButton(
BackupIconButton(
modifier = Modifier.fillMaxWidth(),
icon = { Icon(Icons.Outlined.Lock, contentDescription = null, modifier = Modifier.size(18.dp)) },
text = text,
colors = ButtonDefaults.outlinedButtonColors(),
onClick = {
authenticate(
title = text,
@@ -540,21 +662,17 @@ private fun SeedWordsButton(
onApproved = {
Amber.instance.applicationIOScope.launch {
account.didBackup = true
onBackupDone()
showSeedWords.value = true
}
},
onError = { _, message ->
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
message,
Toast.LENGTH_SHORT,
).show()
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
},
)
},
text = text,
)
}
@@ -562,26 +680,30 @@ private fun SeedWordsButton(
private fun NSecCopyButton(
account: Account,
password: String,
modifier: Modifier = Modifier,
onLoading: (Boolean) -> Unit,
onBackupDone: () -> Unit,
) {
val clipboardManager = LocalClipboard.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val keyguardLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
if (password.isNotBlank()) {
encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading)
} else {
copyNSec(account, clipboardManager)
}
val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
if (password.isNotBlank()) {
encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading, onBackupDone)
} else {
copyNSec(account, clipboardManager, onBackupDone)
}
}
}
val text = stringResource(if (password.isBlank()) R.string.copy_to_clipboard else R.string.encrypt_and_copy_to_clipboard)
AmberButton(
BackupIconButton(
modifier = modifier,
icon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp)) },
text = text,
onClick = {
authenticate(
title = text,
@@ -590,33 +712,63 @@ private fun NSecCopyButton(
onApproved = {
scope.launch(Dispatchers.IO) {
account.didBackup = true
onBackupDone()
if (password.isNotBlank()) {
encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading)
encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading, onBackupDone)
} else {
copyNSec(account, clipboardManager)
copyNSec(account, clipboardManager, onBackupDone)
}
}
},
onError = { _, message ->
scope.launch {
Toast.makeText(
context,
message,
Toast.LENGTH_SHORT,
).show()
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
},
)
},
text = text,
)
}
@Composable
private fun BackupIconButton(
modifier: Modifier = Modifier,
icon: @Composable () -> Unit,
text: String,
colors: ButtonColors = ButtonDefaults.buttonColors().copy(contentColor = Color.Black),
onClick: () -> Unit,
) {
Button(
onClick = onClick,
modifier = modifier
.height(52.dp),
shape = RoundedCornerShape(12.dp),
colors = colors,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
) {
CompositionLocalProvider(
LocalDensity provides Density(LocalDensity.current.density, 1f),
) {
icon()
Spacer(modifier = Modifier.width(6.dp))
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
maxLines = 2,
)
}
}
}
private fun copyNSec(
account: Account,
clipboardManager: Clipboard,
onBackupDone: () -> Unit,
) {
account.copyToClipboard(clipboardManager)
onBackupDone()
}
private fun encryptCopyNSec(
@@ -625,15 +777,11 @@ private fun encryptCopyNSec(
account: Account,
clipboardManager: Clipboard,
onLoading: (Boolean) -> Unit,
onBackupDone: () -> Unit,
) {
if (password.isBlank()) {
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.password_is_required),
Toast.LENGTH_SHORT,
)
.show()
Toast.makeText(context, context.getString(R.string.password_is_required), Toast.LENGTH_SHORT).show()
}
} else {
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
@@ -641,28 +789,16 @@ private fun encryptCopyNSec(
try {
val key = account.nip49Encrypt(password)
account.didBackup = true
onBackupDone()
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText("", key),
),
)
Toast.makeText(
context,
context.getString(R.string.secret_key_copied_to_clipboard),
Toast.LENGTH_SHORT,
).show()
clipboardManager.setClipEntry(ClipEntry(ClipData.newPlainText("", key)))
Toast.makeText(context, context.getString(R.string.secret_key_copied_to_clipboard), Toast.LENGTH_SHORT).show()
}
onLoading(false)
} catch (_: Exception) {
onLoading(false)
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.failed_to_encrypt_key),
Toast.LENGTH_SHORT,
).show()
Toast.makeText(context, context.getString(R.string.failed_to_encrypt_key), Toast.LENGTH_SHORT).show()
}
}
}
+2
View File
@@ -23,6 +23,8 @@
<string name="nsec">Nsec / private key</string>
<string name="show_password">Show password</string>
<string name="hide_password">Hide password</string>
<string name="hide">Hide</string>
<string name="show_tips">Tips</string>
<string name="invalid_key">Invalid key</string>
<string name="key_is_required">Key is required</string>
<string name="login">Login</string>