mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Merge pull request #315 from greenart7c3/claude/improve-backup-interface-pmUHz
Refactor AccountBackupScreen UI with modular components
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user