Merge pull request #315 from greenart7c3/claude/improve-backup-interface-pmUHz

Refactor AccountBackupScreen UI with modular components
This commit is contained in:
greenart7c3
2026-03-09 07:08:14 -03:00
committed by GitHub
3 changed files with 421 additions and 285 deletions
@@ -455,7 +455,11 @@ fun MainScreen(
.padding(horizontal = verticalPadding)
.padding(top = verticalPadding * 1.5f)
.imePadding(),
navController,
onShowQrCode = {
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
navController.navigate(Route.QrCode.route.replace("{content}", it))
}
},
)
},
)
@@ -8,26 +8,41 @@ 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
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.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.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,11 +83,11 @@ 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
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavHostController
import coil3.compose.SubcomposeAsyncImage
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
@@ -83,10 +99,8 @@ 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
import com.greenart7c3.nostrsigner.ui.theme.Size35dp
import com.greenart7c3.nostrsigner.ui.theme.fromHex
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
@@ -103,211 +117,328 @@ import kotlinx.coroutines.launch
@Composable
fun AccountBackupScreen(
modifier: Modifier,
navController: NavHostController,
onShowQrCode: (String) -> Unit,
) {
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 },
onShowQrCode = onShowQrCode,
)
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,
onShowQrCode: (String) -> Unit,
) {
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 },
onShowQrCode = onShowQrCode,
)
}
// 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 +448,56 @@ fun AccountBackupScreen(
private fun NSecQrButton(
account: Account,
password: String,
modifier: Modifier = Modifier,
onLoading: (Boolean) -> Unit,
navController: NavHostController,
onBackupDone: () -> Unit,
onShowQrCode: (String) -> Unit,
) {
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)
}
} else {
showDialog.value = true
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()
onShowQrCode(ncryptsec)
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,9 +509,8 @@ private fun NSecQrButton(
onLoading(true)
val ncryptsec = account.nip49Encrypt(password)
account.didBackup = true
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec))
}
onBackupDone()
onShowQrCode(ncryptsec)
onLoading(false)
}
} else {
@@ -391,17 +519,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 +533,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 +566,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 +592,6 @@ fun QrCodeDialog(
) {
CloseButton(onCancel = onClose)
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -491,34 +609,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 +643,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 +656,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 +674,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 +706,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 +771,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 +783,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>