Start work on edge to edge

This commit is contained in:
greenart7c3
2024-09-06 12:20:59 -03:00
parent cee398b5b2
commit f914d55203
8 changed files with 518 additions and 388 deletions
@@ -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,
)
}