From f914d55203ddda3364181c3aaac48c4d5b23efe1 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 6 Sep 2024 12:20:59 -0300 Subject: [PATCH] Start work on edge to edge --- .../greenart7c3/nostrsigner/MainActivity.kt | 6 +- .../greenart7c3/nostrsigner/ui/LoginScreen.kt | 4 +- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 304 +++++------------ .../nostrsigner/ui/SettingsScreen.kt | 11 +- .../ui/actions/AccountBackupDialog.kt | 305 +++++++++--------- .../ui/components/BackButtonScaffold.kt | 44 +++ .../nostrsigner/ui/components/MainScaffold.kt | 225 +++++++++++++ .../nostrsigner/ui/navigation/Route.kt | 7 + 8 files changed, 518 insertions(+), 388 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BackButtonScaffold.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MainScaffold.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt index 5193226b..e2ddeaa5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/MainActivity.kt @@ -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}") diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt index eb491280..a2659904 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt @@ -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 = { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 77a743cd..29d1f35b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -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, ) - }, - ) - } + } + }, + ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt index e408556c..5e68e95c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt @@ -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 diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt index 15841567..928147af 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt @@ -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) } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BackButtonScaffold.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BackButtonScaffold.kt new file mode 100644 index 00000000..32f86a83 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BackButtonScaffold.kt @@ -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) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MainScaffold.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MainScaffold.kt new file mode 100644 index 00000000..3a821f92 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MainScaffold.kt @@ -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) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt index e6e9bf41..dcd6ab9f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt @@ -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, + ) }