mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Start work on edge to edge
This commit is contained in:
@@ -8,6 +8,7 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -49,10 +50,11 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var mainViewModel: MainViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
installSplashScreen()
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
mainViewModel = MainViewModel(applicationContext)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
installSplashScreen()
|
||||
|
||||
HttpClientManager.setDefaultUserAgent("Amber/${BuildConfig.VERSION_NAME}")
|
||||
|
||||
|
||||
@@ -132,9 +132,9 @@ fun MainPage(
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ElevatedButton(
|
||||
onClick = {
|
||||
|
||||
@@ -16,10 +16,7 @@ import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -32,25 +29,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Hub
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.RichTooltip
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -100,16 +86,14 @@ import com.greenart7c3.nostrsigner.service.EventNotificationConsumer
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
|
||||
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ActiveRelaysDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupScreen
|
||||
import com.greenart7c3.nostrsigner.ui.components.BackButtonScaffold
|
||||
import com.greenart7c3.nostrsigner.ui.components.MainScaffold
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Base64
|
||||
import java.util.UUID
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -138,11 +122,7 @@ fun sendResult(
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val defaultRelays = NostrSigner.getInstance().settings.defaultRelays
|
||||
val savedApplication = database.applicationDao().getByKey(key)
|
||||
val relays = if (savedApplication != null) {
|
||||
savedApplication.application.relays.ifEmpty { defaultRelays }
|
||||
} else {
|
||||
intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays
|
||||
}
|
||||
val relays = savedApplication?.application?.relays?.ifEmpty { defaultRelays } ?: (intentData.bunkerRequest?.relays?.ifEmpty { defaultRelays } ?: defaultRelays)
|
||||
if (intentData.bunkerRequest != null) {
|
||||
NostrSigner.getInstance().checkForNewRelays(
|
||||
NostrSigner.getInstance().settings.notificationType != NotificationType.DIRECT,
|
||||
@@ -568,7 +548,6 @@ private suspend fun initNotifications(context: Context) {
|
||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts(context))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
account: Account,
|
||||
@@ -582,17 +561,7 @@ fun MainScreen(
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val destinationRoute = navBackStackEntry?.destination?.route ?: ""
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.Settings)
|
||||
var shouldShowBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
confirmValueChange = { it != SheetValue.PartiallyExpanded },
|
||||
skipPartiallyExpanded = true,
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val requestPermissionLauncher =
|
||||
@@ -659,173 +628,37 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (destinationRoute == "Permissions" && BuildConfig.FLAVOR != "offline") {
|
||||
PermissionsFloatingActionButton(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
) {
|
||||
val secret = UUID.randomUUID().toString().substring(0, 6)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val application =
|
||||
ApplicationEntity(
|
||||
secret,
|
||||
"",
|
||||
NostrSigner.getInstance().settings.defaultRelays,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
false,
|
||||
secret,
|
||||
false,
|
||||
account.signPolicy,
|
||||
)
|
||||
var localRoute by remember { mutableStateOf(route.value ?: Route.Home.route) }
|
||||
|
||||
database.applicationDao().insertApplication(
|
||||
application,
|
||||
)
|
||||
val relayString = NostrSigner.getInstance().settings.defaultRelays.joinToString(separator = "&") { "relay=${it.url}" }
|
||||
val bunkerUrl = "bunker://${account.keyPair.pubKey.toHexKey()}?$relayString"
|
||||
clipboardManager.setText(AnnotatedString(bunkerUrl))
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigate("Permission/$secret")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
if (shouldShowBottomSheet) {
|
||||
AccountsBottomSheet(
|
||||
sheetState = sheetState,
|
||||
account = account,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
onClose = {
|
||||
scope.launch {
|
||||
shouldShowBottomSheet = false
|
||||
sheetState.hide()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
actions = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
var showDialogRelay by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDialogRelay) {
|
||||
ActiveRelaysDialog(
|
||||
onClose = {
|
||||
showDialogRelay = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(40.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
showDialogRelay = true
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Hub,
|
||||
contentDescription = "Relays",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
val name = LocalPreferences.getAccountName(context, account.keyPair.pubKey.toNpub())
|
||||
Row(
|
||||
Modifier
|
||||
.border(
|
||||
border = ButtonDefaults.outlinedButtonBorder,
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
.padding(8.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
shouldShowBottomSheet = true
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(name.ifBlank { account.keyPair.pubKey.toNpub().toShortenHex() })
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
if (!destinationRoute.contains("Permission/")) {
|
||||
NavigationBar(tonalElevation = 0.dp) {
|
||||
items.forEach {
|
||||
val selected = destinationRoute == it.route
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(it.route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
if (selected) it.selectedIcon else it.icon,
|
||||
it.route,
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(it.title)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
var localRoute by remember { mutableStateOf(route.value ?: Route.Home.route) }
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
LaunchedEffect(Unit, route.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
if (route.value != null) {
|
||||
localRoute = route.value!!
|
||||
route.value = null
|
||||
}
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
LaunchedEffect(Unit, route.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
if (route.value != null) {
|
||||
localRoute = route.value!!
|
||||
route.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = localRoute,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
||||
) {
|
||||
composable(
|
||||
Route.Home.route,
|
||||
content = {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = localRoute,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
||||
) {
|
||||
composable(
|
||||
Route.Home.route,
|
||||
content = {
|
||||
MainScaffold(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
database,
|
||||
navController,
|
||||
destinationRoute,
|
||||
false,
|
||||
) { padding ->
|
||||
HomeScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -836,12 +669,21 @@ fun MainScreen(
|
||||
account,
|
||||
database,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permissions.route,
|
||||
content = {
|
||||
composable(
|
||||
Route.Permissions.route,
|
||||
content = {
|
||||
MainScaffold(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
database,
|
||||
navController,
|
||||
destinationRoute,
|
||||
true,
|
||||
) { padding ->
|
||||
PermissionsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -851,26 +693,56 @@ fun MainScreen(
|
||||
navController = navController,
|
||||
database = database,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Settings.route,
|
||||
content = {
|
||||
composable(
|
||||
Route.Settings.route,
|
||||
content = {
|
||||
MainScaffold(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
database,
|
||||
navController,
|
||||
destinationRoute,
|
||||
false,
|
||||
) { padding ->
|
||||
SettingsScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
accountStateViewModel,
|
||||
account,
|
||||
navController,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permission.route,
|
||||
arguments = listOf(navArgument("packageName") { type = NavType.StringType }),
|
||||
content = {
|
||||
composable(
|
||||
Route.AccountBackup.route,
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
AccountBackupScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
account,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permission.route,
|
||||
arguments = listOf(navArgument("packageName") { type = NavType.StringType }),
|
||||
content = {
|
||||
BackButtonScaffold(
|
||||
navController = navController,
|
||||
) { padding ->
|
||||
EditPermission(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -882,8 +754,8 @@ fun MainScreen(
|
||||
navController = navController,
|
||||
database = database,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
@@ -76,7 +77,6 @@ import com.greenart7c3.nostrsigner.service.Biometrics
|
||||
import com.greenart7c3.nostrsigner.service.ConnectivityService
|
||||
import com.greenart7c3.nostrsigner.service.NotificationDataSource
|
||||
import com.greenart7c3.nostrsigner.service.PushNotificationUtils
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ConnectOrbotDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.EditDefaultRelaysDialog
|
||||
import com.greenart7c3.nostrsigner.ui.actions.LogoutDialog
|
||||
@@ -86,6 +86,7 @@ import com.greenart7c3.nostrsigner.ui.components.IconRow
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.TextSpinner
|
||||
import com.greenart7c3.nostrsigner.ui.components.TitleExplainer
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.vitorpamplona.ammolite.relays.RelayPool
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import java.io.IOException
|
||||
@@ -158,8 +159,8 @@ fun SettingsScreen(
|
||||
modifier: Modifier,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
account: Account,
|
||||
navController: NavController,
|
||||
) {
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
var checked by remember { mutableStateOf(account.useProxy) }
|
||||
@@ -643,7 +644,7 @@ fun SettingsScreen(
|
||||
icon = Icons.Default.Key,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
backupDialogOpen = true
|
||||
navController.navigate(Route.AccountBackup.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -832,10 +833,6 @@ fun SettingsScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -22,7 +22,6 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
@@ -86,9 +85,9 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun AccountBackupDialog(
|
||||
fun AccountBackupScreen(
|
||||
modifier: Modifier,
|
||||
account: Account,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
var showSeedWords by remember { mutableStateOf(false) }
|
||||
if (showSeedWords) {
|
||||
@@ -122,189 +121,173 @@ fun AccountBackupDialog(
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
Surface(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize(),
|
||||
.padding(horizontal = 30.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
val content = stringResource(R.string.account_backup_tips_md)
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
CloseButton(onCancel = onClose)
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 30.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val content = stringResource(R.string.account_backup_tips_md)
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
if (account.seedWords.isNotEmpty()) {
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
showSeedWords = true
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
) {
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
if (account.seedWords.isNotEmpty()) {
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
Button(
|
||||
onClick = {
|
||||
authenticate(
|
||||
title = context.getString(R.string.show_seed_words),
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
showSeedWords = true
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
authenticate(
|
||||
title = context.getString(R.string.show_seed_words),
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
showSeedWords = true
|
||||
},
|
||||
onError = { _, message ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.List,
|
||||
contentDescription = stringResource(R.string.show_seed_words),
|
||||
)
|
||||
Text(text = stringResource(R.string.show_seed_words))
|
||||
}
|
||||
}
|
||||
}
|
||||
NSecCopyButton(account)
|
||||
NSecQrButton(account)
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
val content1 = stringResource(R.string.account_backup_tips3_md)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.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 autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { password.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.ncryptsec_password),
|
||||
},
|
||||
onError = { _, message ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
},
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.List,
|
||||
contentDescription = stringResource(R.string.show_seed_words),
|
||||
)
|
||||
Text(text = stringResource(R.string.show_seed_words))
|
||||
}
|
||||
}
|
||||
}
|
||||
NSecCopyButton(account)
|
||||
NSecQrButton(account)
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
val content1 = stringResource(R.string.account_backup_tips3_md)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.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 autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { password.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
},
|
||||
visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
)
|
||||
|
||||
if (errorMessage.isNotBlank()) {
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
text = stringResource(R.string.ncryptsec_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(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
EncryptNSecCopyButton(account, password)
|
||||
EncryptNSecQRButton(account, password)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
EncryptNSecCopyButton(account, password)
|
||||
EncryptNSecQRButton(account, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavController
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BackButtonScaffold(
|
||||
navController: NavController,
|
||||
content: @Composable (paddingValues: PaddingValues) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("")
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
navController.navigateUp()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
content(padding)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Hub
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.NostrSigner
|
||||
import com.greenart7c3.nostrsigner.database.AppDatabase
|
||||
import com.greenart7c3.nostrsigner.database.ApplicationEntity
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
|
||||
import com.greenart7c3.nostrsigner.ui.PermissionsFloatingActionButton
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ActiveRelaysDialog
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScaffold(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
account: Account,
|
||||
database: AppDatabase,
|
||||
navController: NavController,
|
||||
destinationRoute: String,
|
||||
shouldShowFloatingButton: Boolean,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.Settings)
|
||||
var shouldShowBottomSheet by remember { mutableStateOf(false) }
|
||||
val sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
confirmValueChange = { it != SheetValue.PartiallyExpanded },
|
||||
skipPartiallyExpanded = true,
|
||||
)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline" && shouldShowFloatingButton) {
|
||||
PermissionsFloatingActionButton(
|
||||
accountStateViewModel,
|
||||
account,
|
||||
) {
|
||||
val secret = UUID.randomUUID().toString().substring(0, 6)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val application =
|
||||
ApplicationEntity(
|
||||
secret,
|
||||
"",
|
||||
NostrSigner.getInstance().settings.defaultRelays,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
false,
|
||||
secret,
|
||||
false,
|
||||
account.signPolicy,
|
||||
)
|
||||
|
||||
database.applicationDao().insertApplication(
|
||||
application,
|
||||
)
|
||||
val relayString = NostrSigner.getInstance().settings.defaultRelays.joinToString(separator = "&") { "relay=${it.url}" }
|
||||
val bunkerUrl = "bunker://${account.keyPair.pubKey.toHexKey()}?$relayString"
|
||||
clipboardManager.setText(AnnotatedString(bunkerUrl))
|
||||
scope.launch(Dispatchers.Main) {
|
||||
navController.navigate("Permission/$secret")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
if (shouldShowBottomSheet) {
|
||||
AccountsBottomSheet(
|
||||
sheetState = sheetState,
|
||||
account = account,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
onClose = {
|
||||
scope.launch {
|
||||
shouldShowBottomSheet = false
|
||||
sheetState.hide()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
actions = {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR != "offline") {
|
||||
var showDialogRelay by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDialogRelay) {
|
||||
ActiveRelaysDialog(
|
||||
onClose = {
|
||||
showDialogRelay = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(40.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
showDialogRelay = true
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Hub,
|
||||
contentDescription = "Relays",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
val name = LocalPreferences.getAccountName(context, account.keyPair.pubKey.toNpub())
|
||||
Row(
|
||||
Modifier
|
||||
.border(
|
||||
border = ButtonDefaults.outlinedButtonBorder,
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
.padding(8.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
) {
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
shouldShowBottomSheet = true
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(name.ifBlank { account.keyPair.pubKey.toNpub().toShortenHex() })
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar(tonalElevation = 0.dp) {
|
||||
items.forEach {
|
||||
val selected = destinationRoute == it.route
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(it.route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
if (selected) it.selectedIcon else it.icon,
|
||||
it.route,
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(it.title)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
content(padding)
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,11 @@ sealed class Route(
|
||||
icon = Icons.AutoMirrored.Outlined.ViewList,
|
||||
selectedIcon = Icons.AutoMirrored.Default.ViewList,
|
||||
)
|
||||
|
||||
data object AccountBackup : Route(
|
||||
title = "",
|
||||
route = "AccountBackup",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user