mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 03:18:23 +00:00
Refactor bunker and login request screens to support account selection and improved permission management.
- Add tabbed navigation to `BunkerConnectRequestScreen` and `LoginWithPubKey` to separate login and permission settings. - Implement account selection in the login tab, allowing users to choose which account to connect. - Move permission toggle list to a modal bottom sheet for a cleaner UI. - Update `AmberTopAppBar` to display application icons and improved titles for incoming requests. - Add `SignerConnectAppTab` component for consistent tab styling. - Update `LocalPreferences` to use non-suspending versions of account loading methods. - Refactor `AmberBunkerRequest` to include `isNostrConnectUri` flag. - Improve `EnabledPermissions` by using a `LazyColumn` for better performance. - Update string resources for signing policy explainers.
This commit is contained in:
@@ -186,7 +186,7 @@ object LocalPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reloadApp() {
|
||||
fun reloadApp() {
|
||||
val context = Amber.instance
|
||||
currentAccount = null
|
||||
savedAccounts = null
|
||||
@@ -429,14 +429,14 @@ object LocalPreferences {
|
||||
accountCache.put(account.npub, account)
|
||||
}
|
||||
|
||||
suspend fun loadFromEncryptedStorage(context: Context): Account? {
|
||||
fun loadFromEncryptedStorage(context: Context): Account? {
|
||||
currentAccount(context)?.let {
|
||||
return loadFromEncryptedStorage(context, it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun allAccounts(context: Context): List<Account> {
|
||||
fun allAccounts(context: Context): List<Account> {
|
||||
val accountInfos = allSavedAccounts(context)
|
||||
return accountInfos.mapNotNull {
|
||||
loadFromEncryptedStorage(context, it.npub)
|
||||
@@ -493,7 +493,7 @@ object LocalPreferences {
|
||||
HttpClientManager.setDefaultProxyOnPort(port)
|
||||
}
|
||||
|
||||
suspend fun loadFromEncryptedStorage(context: Context, npub: String): Account? {
|
||||
fun loadFromEncryptedStorage(context: Context, npub: String): Account? {
|
||||
if (accountCache.get(npub) != null) {
|
||||
return accountCache.get(npub)
|
||||
}
|
||||
|
||||
@@ -25,4 +25,5 @@ data class AmberBunkerRequest(
|
||||
val checked: MutableState<Boolean> = mutableStateOf(true),
|
||||
val rememberType: MutableState<RememberType> = mutableStateOf(RememberType.NEVER),
|
||||
val encryptionType: EncryptionType,
|
||||
val isNostrConnectUri: Boolean,
|
||||
)
|
||||
|
||||
@@ -207,6 +207,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
signedEvent = signedEvent,
|
||||
encryptedData = encryptedDataKind,
|
||||
encryptionType = encryptionType,
|
||||
isNostrConnectUri = false,
|
||||
)
|
||||
|
||||
var amberEvent: AmberEvent? = null
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.greenart7c3.nostrsigner.models.containsNip
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NostrConnectUtils {
|
||||
@@ -22,6 +23,10 @@ object NostrConnectUtils {
|
||||
intent: Intent,
|
||||
account: Account,
|
||||
) {
|
||||
if (intent.extras?.getString("id") == null) {
|
||||
intent.putExtra("id", UUID.randomUUID().toString().substring(0, 6))
|
||||
}
|
||||
|
||||
try {
|
||||
val data = intent.dataString.toString().replace("nostrconnect://", "")
|
||||
val split = data.split("?")
|
||||
@@ -107,6 +112,7 @@ object NostrConnectUtils {
|
||||
BunkerRequestUtils.addRequest(
|
||||
AmberBunkerRequest(
|
||||
BunkerRequestConnect(
|
||||
id = intent.extras?.getString("id") ?: UUID.randomUUID().toString().substring(0, 6),
|
||||
remoteKey = pubKey,
|
||||
secret = "",
|
||||
permissions = if (permissions.isNotEmpty()) {
|
||||
@@ -124,6 +130,7 @@ object NostrConnectUtils {
|
||||
signedEvent = null,
|
||||
encryptedData = null,
|
||||
encryptionType = EncryptionType.NIP44,
|
||||
isNostrConnectUri = true,
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -340,14 +340,24 @@ fun MainScreen(
|
||||
Route.IncomingRequest.route,
|
||||
content = {
|
||||
val scrollState = rememberScrollState()
|
||||
IncomingRequestScreen(
|
||||
modifier = Modifier
|
||||
val modifier = if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScrollbar(scrollState)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = verticalPadding)
|
||||
.padding(top = verticalPadding * 1.5f),
|
||||
.padding(top = verticalPadding * 1.5f)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScrollbar(scrollState)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = verticalPadding)
|
||||
}
|
||||
IncomingRequestScreen(
|
||||
modifier = modifier,
|
||||
intents = intents,
|
||||
bunkerRequests = bunkerRequests,
|
||||
packageName = packageName,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Shield
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
@@ -26,6 +28,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -41,6 +44,7 @@ import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.routes
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||
import java.util.Base64
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -59,154 +63,183 @@ fun AmberTopAppBar(
|
||||
bunkerRequests: List<AmberBunkerRequest>,
|
||||
packageName: String?,
|
||||
) {
|
||||
if (destinationRoute != "login" && destinationRoute != "create" && destinationRoute != "loginPage") {
|
||||
CenterAlignedTopAppBar(
|
||||
actions = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(text = context.getString(R.string.reconnect))
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
Amber.instance.reconnect()
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val relayStats = Amber.instance.client.relayStatusFlow().collectAsStateWithLifecycle()
|
||||
Text("${relayStats.value.connected.size}/${relayStats.value.available.size}")
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.relays),
|
||||
contentDescription = context.getString(R.string.reconnect),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (Amber.instance.settings.useProxy) {
|
||||
var isProxyEnabled by remember { mutableStateOf(false) }
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort)
|
||||
}
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(text = if (isProxyEnabled) context.getString(R.string.proxy_is_connected) else context.getString(R.string.proxy_is_not_working))
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) {
|
||||
if (destinationRoute != "login" && destinationRoute != "create" && destinationRoute != "loginPage") {
|
||||
CenterAlignedTopAppBar(
|
||||
actions = {
|
||||
if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (isProxyEnabled) {
|
||||
Toast.makeText(context, context.getString(R.string.proxy_is_connected), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, context.getString(R.string.proxy_is_not_working), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(text = context.getString(R.string.reconnect))
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
Icons.Outlined.Shield,
|
||||
context.getString(R.string.proxy),
|
||||
tint = if (isProxyEnabled) Color.Green else Color.Red,
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
Amber.instance.reconnect()
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val relayStats = Amber.instance.client.relayStatusFlow().collectAsStateWithLifecycle()
|
||||
Text("${relayStats.value.connected.size}/${relayStats.value.available.size}")
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.relays),
|
||||
contentDescription = context.getString(R.string.reconnect),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (Amber.instance.settings.useProxy) {
|
||||
var isProxyEnabled by remember { mutableStateOf(false) }
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort)
|
||||
}
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(text = if (isProxyEnabled) context.getString(R.string.proxy_is_connected) else context.getString(R.string.proxy_is_not_working))
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (isProxyEnabled) {
|
||||
Toast.makeText(context, context.getString(R.string.proxy_is_connected), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, context.getString(R.string.proxy_is_not_working), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
Icons.Outlined.Shield,
|
||||
context.getString(R.string.proxy),
|
||||
tint = if (isProxyEnabled) Color.Green else Color.Red,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
var title by remember { mutableStateOf(routes.find { it.route.startsWith(destinationRoute) }?.title ?: "") }
|
||||
LaunchedEffect(destinationRoute) {
|
||||
if (destinationRoute.startsWith("Permission/") || destinationRoute.startsWith("Activity/") || destinationRoute.startsWith("RelayLogScreen/") || destinationRoute.startsWith("qrcode/")) {
|
||||
launch(Dispatchers.IO) {
|
||||
navBackStackEntry?.arguments?.getString("content")?.let {
|
||||
title = Route.QrCode.title
|
||||
}
|
||||
navBackStackEntry?.arguments?.getString("packageName")?.let { packageName ->
|
||||
val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application
|
||||
title = if (destinationRoute.startsWith("Activity/")) {
|
||||
"${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}"
|
||||
} else {
|
||||
application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName
|
||||
}
|
||||
}
|
||||
navBackStackEntry?.arguments?.getString("key")?.let { packageName ->
|
||||
val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application
|
||||
title = if (destinationRoute.startsWith("Activity/")) {
|
||||
"${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}"
|
||||
} else {
|
||||
application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName
|
||||
}
|
||||
}
|
||||
navBackStackEntry?.arguments?.getString("url")?.let { url ->
|
||||
val localUrl = Base64.getDecoder().decode(url).toString(Charsets.UTF_8)
|
||||
title = localUrl
|
||||
}
|
||||
}
|
||||
} else {
|
||||
launch(Dispatchers.IO) {
|
||||
if (destinationRoute == Route.IncomingRequest.route && (intents.isNotEmpty() || bunkerRequests.isNotEmpty())) {
|
||||
val key = if (bunkerRequests.isNotEmpty()) {
|
||||
bunkerRequests.first().localKey
|
||||
} else {
|
||||
packageName
|
||||
}
|
||||
|
||||
val application = Amber.instance.getDatabase(account.npub).dao().getByKey(key ?: "")?.application
|
||||
val titleTemp = application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName ?: ""
|
||||
title = if (titleTemp.isBlank()) {
|
||||
routes.find { it.route == destinationRoute }?.title ?: ""
|
||||
} else {
|
||||
"$titleTemp - Request"
|
||||
}
|
||||
} else {
|
||||
title = routes.find { it.route == destinationRoute }?.title ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
var title by remember { mutableStateOf(routes.find { it.route.startsWith(destinationRoute) }?.title ?: "") }
|
||||
LaunchedEffect(destinationRoute) {
|
||||
if (destinationRoute.startsWith("Permission/") || destinationRoute.startsWith("Activity/") || destinationRoute.startsWith("RelayLogScreen/") || destinationRoute.startsWith("qrcode/")) {
|
||||
launch(Dispatchers.IO) {
|
||||
navBackStackEntry?.arguments?.getString("content")?.let {
|
||||
title = Route.QrCode.title
|
||||
}
|
||||
navBackStackEntry?.arguments?.getString("packageName")?.let { packageName ->
|
||||
val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application
|
||||
title = if (destinationRoute.startsWith("Activity/")) {
|
||||
"${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}"
|
||||
} else {
|
||||
application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName
|
||||
}
|
||||
}
|
||||
navBackStackEntry?.arguments?.getString("key")?.let { packageName ->
|
||||
val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application
|
||||
title = if (destinationRoute.startsWith("Activity/")) {
|
||||
"${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}"
|
||||
} else {
|
||||
application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName
|
||||
}
|
||||
}
|
||||
navBackStackEntry?.arguments?.getString("url")?.let { url ->
|
||||
val localUrl = Base64.getDecoder().decode(url).toString(Charsets.UTF_8)
|
||||
title = localUrl
|
||||
}
|
||||
}
|
||||
} else {
|
||||
launch(Dispatchers.IO) {
|
||||
if (destinationRoute == Route.IncomingRequest.route && (bunkerRequests.isNotEmpty())) {
|
||||
val request = bunkerRequests.first()
|
||||
val key = bunkerRequests.first().localKey
|
||||
|
||||
Text(title)
|
||||
},
|
||||
)
|
||||
var application = Amber.instance.getDatabase(account.npub).dao().getByKey(key)?.application
|
||||
if (application == null && request.request is BunkerRequestConnect) {
|
||||
val secret = request.request.secret
|
||||
if (secret != null) {
|
||||
application = Amber.instance.getDatabase(account.npub).dao().getByKey(secret)?.application
|
||||
}
|
||||
}
|
||||
val titleTemp = application?.name?.ifBlank { request.name.ifBlank { application.key.toShortenHex() } } ?: request.name
|
||||
title = titleTemp.ifBlank {
|
||||
routes.find { it.route == destinationRoute }?.title ?: ""
|
||||
}
|
||||
} else {
|
||||
title = routes.find { it.route == destinationRoute }?.title ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(title)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberAppDisplayInfo(packageName: String): AppDisplayInfo {
|
||||
val context = LocalContext.current
|
||||
return remember(packageName) {
|
||||
val appInfo = runCatching {
|
||||
context.packageManager.getApplicationInfo(packageName, 0)
|
||||
}.getOrNull()
|
||||
|
||||
if (appInfo != null) {
|
||||
AppDisplayInfo(
|
||||
name = context.packageManager.getApplicationLabel(appInfo).toString(),
|
||||
icon = context.packageManager.getApplicationIcon(appInfo),
|
||||
)
|
||||
} else {
|
||||
AppDisplayInfo(name = packageName, icon = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AppDisplayInfo(
|
||||
val name: String,
|
||||
val icon: Drawable? = null,
|
||||
)
|
||||
|
||||
+239
-85
@@ -1,5 +1,7 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -7,11 +9,26 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
@@ -21,15 +38,23 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.greenart7c3.nostrsigner.Amber
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.AmberBunkerRequest
|
||||
import com.greenart7c3.nostrsigner.models.Permission
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.DeleteAfterType
|
||||
import com.greenart7c3.nostrsigner.ui.RememberType
|
||||
import com.greenart7c3.nostrsigner.ui.SettingsRow
|
||||
@@ -38,14 +63,15 @@ import com.greenart7c3.nostrsigner.ui.parseDeleteAfterType
|
||||
import kotlin.collections.forEach
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BunkerConnectRequestScreen(
|
||||
modifier: Modifier,
|
||||
shouldCloseApp: Boolean,
|
||||
account: Account,
|
||||
appName: String,
|
||||
bunkerRequest: AmberBunkerRequest,
|
||||
permissions: List<Permission>?,
|
||||
onAccept: (List<Permission>?, Int, Boolean?, RememberType, Long) -> Unit,
|
||||
onAccept: (List<Permission>?, Int, Boolean?, RememberType, Long, Account) -> Unit,
|
||||
onReject: (RememberType) -> Unit,
|
||||
) {
|
||||
val localPermissions = remember {
|
||||
@@ -55,117 +81,245 @@ fun BunkerConnectRequestScreen(
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
|
||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||
var selectedOption by remember { mutableIntStateOf(account.signPolicy) }
|
||||
val accounts = remember {
|
||||
val snapshot = mutableStateListOf<Account>()
|
||||
if (bunkerRequest.isNostrConnectUri) {
|
||||
LocalPreferences.allAccounts(Amber.instance).forEach {
|
||||
snapshot.add(it)
|
||||
}
|
||||
} else {
|
||||
snapshot.add(account)
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
var selectedAccountIndex by remember { mutableIntStateOf(accounts.indexOf(account)) }
|
||||
var rememberType by remember { mutableStateOf(RememberType.NEVER) }
|
||||
val deleteAfterItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringResource(DeleteAfterType.NEVER.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.FIVE_MINUTES.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.TEN_MINUTES.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.ONE_HOUR.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.ONE_DAY.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.ONE_WEEK.resourceId)),
|
||||
)
|
||||
var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) }
|
||||
var closeApp by remember { mutableStateOf(shouldCloseApp) }
|
||||
|
||||
Column(
|
||||
modifier,
|
||||
) {
|
||||
ProfilePicture(account)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = appName,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf),
|
||||
)
|
||||
|
||||
var selectedOption by remember { mutableIntStateOf(account.signPolicy) }
|
||||
val deleteAfterItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringResource(DeleteAfterType.NEVER.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.FIVE_MINUTES.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.TEN_MINUTES.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.ONE_HOUR.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.ONE_DAY.resourceId)),
|
||||
TitleExplainer(stringResource(DeleteAfterType.ONE_WEEK.resourceId)),
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
indicator = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.tabIndicatorOffset(selectedTabIndex)
|
||||
.height(6.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
},
|
||||
divider = {
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
},
|
||||
) {
|
||||
SignerConnectAppTab(
|
||||
text = stringResource(id = R.string.login).uppercase(),
|
||||
selected = selectedTabIndex == 0,
|
||||
onClick = {
|
||||
selectedTabIndex = 0
|
||||
},
|
||||
)
|
||||
var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) }
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.handle_application_permissions),
|
||||
)
|
||||
SignerConnectAppTab(
|
||||
text = stringResource(id = R.string.permissions).uppercase(),
|
||||
selected = selectedTabIndex == 0,
|
||||
onClick = {
|
||||
selectedTabIndex = 1
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
var closeApp by remember { mutableStateOf(shouldCloseApp) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable {
|
||||
closeApp = !closeApp
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.close_application),
|
||||
)
|
||||
Switch(
|
||||
checked = closeApp,
|
||||
onCheckedChange = {
|
||||
closeApp = it
|
||||
},
|
||||
)
|
||||
}
|
||||
if (selectedTabIndex == 0) {
|
||||
accounts.forEachIndexed { index, acc ->
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (selectedAccountIndex == index) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(4.dp)
|
||||
.selectable(
|
||||
selected = selectedAccountIndex == index,
|
||||
onClick = {
|
||||
selectedAccountIndex = index
|
||||
},
|
||||
),
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
|
||||
Box(
|
||||
Modifier.padding(4.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.delete_after,
|
||||
null,
|
||||
deleteAfterItems,
|
||||
deleteAfterIndex,
|
||||
),
|
||||
leadingContent = {
|
||||
ProfilePictureIcon(
|
||||
account = acc,
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
val name by acc.name.collectAsStateWithLifecycle()
|
||||
Text(
|
||||
name.ifBlank { acc.npub.toShortenHex() },
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
var showModal by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable {
|
||||
closeApp = !closeApp
|
||||
},
|
||||
) {
|
||||
deleteAfterIndex = it
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.close_application),
|
||||
)
|
||||
Switch(
|
||||
checked = closeApp,
|
||||
onCheckedChange = {
|
||||
closeApp = it
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.padding(4.dp),
|
||||
) {
|
||||
SettingsRow(
|
||||
R.string.delete_after,
|
||||
null,
|
||||
deleteAfterItems,
|
||||
deleteAfterIndex,
|
||||
) {
|
||||
deleteAfterIndex = it
|
||||
}
|
||||
}
|
||||
|
||||
ChooseSignPolicy(
|
||||
selectedOption = selectedOption,
|
||||
onSelected = {
|
||||
selectedOption = it
|
||||
},
|
||||
)
|
||||
|
||||
if (selectedOption == 1 && localPermissions.isNotEmpty()) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ElevatedButton(
|
||||
colors = ButtonDefaults.buttonColors().copy(
|
||||
contentColor = Color.Black,
|
||||
),
|
||||
shape = RoundedCornerShape(20),
|
||||
content = {
|
||||
Text(stringResource(R.string.permissions))
|
||||
},
|
||||
onClick = {
|
||||
showModal = true
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showModal) {
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
showModal = false
|
||||
},
|
||||
) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomAppBar {
|
||||
IconRow(
|
||||
center = true,
|
||||
title = stringResource(R.string.go_back),
|
||||
icon = ImageVector.vectorResource(R.drawable.back),
|
||||
onClick = {
|
||||
showModal = false
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
EnabledPermissions(
|
||||
Modifier.padding(it),
|
||||
localPermissions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChooseSignPolicy(
|
||||
selectedOption = selectedOption,
|
||||
onSelected = {
|
||||
selectedOption = it
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Column(
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally,
|
||||
Arrangement.spacedBy(8.dp),
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
if (selectedOption == 1) {
|
||||
EnabledPermissions(localPermissions)
|
||||
}
|
||||
|
||||
AmberButton(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
onClick = {
|
||||
val deleteAfter = deleteAfterToSeconds(parseDeleteAfterType(deleteAfterIndex))
|
||||
onAccept(localPermissions, selectedOption, closeApp, rememberType, deleteAfter)
|
||||
},
|
||||
text = stringResource(R.string.grant_permissions),
|
||||
)
|
||||
|
||||
AmberButton(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 20.dp)
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
onReject(rememberType)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFFF6B00),
|
||||
),
|
||||
text = stringResource(R.string.reject),
|
||||
text = stringResource(R.string.cancel),
|
||||
)
|
||||
|
||||
AmberButton(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 20.dp)
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
val deleteAfter = deleteAfterToSeconds(parseDeleteAfterType(deleteAfterIndex))
|
||||
onAccept(localPermissions, selectedOption, closeApp, rememberType, deleteAfter, accounts[selectedAccountIndex])
|
||||
},
|
||||
text = stringResource(R.string.connect),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-6
@@ -148,6 +148,7 @@ fun BunkerSingleEventHomeScreen(
|
||||
var localRememberType by remember { mutableStateOf(RememberType.NEVER) }
|
||||
var existingAppKey by remember { mutableStateOf("") }
|
||||
var localDeleteAfter by remember { mutableLongStateOf(0L) }
|
||||
var selectedAccount by remember { mutableStateOf<Account>(account) }
|
||||
|
||||
if (showExistingAppDialog) {
|
||||
AlertDialog(
|
||||
@@ -169,7 +170,7 @@ fun BunkerSingleEventHomeScreen(
|
||||
BunkerRequestUtils.sendResult(
|
||||
oldKey = existingAppKey,
|
||||
context = context,
|
||||
account = account,
|
||||
account = selectedAccount,
|
||||
key = key,
|
||||
response = result,
|
||||
bunkerRequest = bunkerRequest,
|
||||
@@ -192,7 +193,7 @@ fun BunkerSingleEventHomeScreen(
|
||||
|
||||
BunkerRequestUtils.sendResult(
|
||||
context = context,
|
||||
account = account,
|
||||
account = selectedAccount,
|
||||
key = key,
|
||||
response = result,
|
||||
bunkerRequest = bunkerRequest,
|
||||
@@ -222,7 +223,7 @@ fun BunkerSingleEventHomeScreen(
|
||||
modifier = modifier,
|
||||
shouldCloseApp = applicationEntity?.application?.closeApplication ?: bunkerRequest.closeApplication,
|
||||
account = account,
|
||||
appName = appName,
|
||||
bunkerRequest = bunkerRequest,
|
||||
permissions = bunkerRequest.request.permissions?.split(",")?.map {
|
||||
val split = it.split(":")
|
||||
if (split.size > 1) {
|
||||
@@ -231,21 +232,22 @@ fun BunkerSingleEventHomeScreen(
|
||||
Permission(split[0].trim(), null)
|
||||
}
|
||||
},
|
||||
onAccept = { permissions, signPolicy, closeApplication, rememberType, deleteAfter ->
|
||||
onAccept = { permissions, signPolicy, closeApplication, rememberType, deleteAfter, acc ->
|
||||
val result = bunkerRequest.nostrConnectSecret.ifBlank { "ack" }
|
||||
|
||||
localPermissions = permissions
|
||||
localSignPolicy = signPolicy
|
||||
localCloseApplication = closeApplication
|
||||
localRememberType = rememberType
|
||||
selectedAccount = acc
|
||||
|
||||
if (bunkerRequest.name.isNotBlank()) {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
val existingApp = Amber.instance.getDatabase(account.npub).dao().getByName(bunkerRequest.name)
|
||||
val existingApp = Amber.instance.getDatabase(acc.npub).dao().getByName(bunkerRequest.name)
|
||||
if (existingApp == null) {
|
||||
BunkerRequestUtils.sendResult(
|
||||
context = context,
|
||||
account = account,
|
||||
account = acc,
|
||||
key = key,
|
||||
response = result,
|
||||
bunkerRequest = bunkerRequest,
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
@@ -23,13 +25,18 @@ import com.greenart7c3.nostrsigner.models.Permission
|
||||
|
||||
@Composable
|
||||
fun EnabledPermissions(
|
||||
modifier: Modifier = Modifier,
|
||||
localPermissions: List<Permission>,
|
||||
) {
|
||||
val enabledPermissions = localPermissions.map {
|
||||
remember { mutableStateOf(it.checked) }
|
||||
}
|
||||
if (localPermissions.isNotEmpty()) {
|
||||
localPermissions.forEachIndexed { index, permission ->
|
||||
LazyColumn(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
) {
|
||||
itemsIndexed(localPermissions) { index, item ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -44,20 +51,20 @@ fun EnabledPermissions(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
permission.checked = !permission.checked
|
||||
enabledPermissions[index].value = permission.checked
|
||||
item.checked = !item.checked
|
||||
enabledPermissions[index].value = item.checked
|
||||
},
|
||||
) {
|
||||
Checkbox(
|
||||
checked = enabledPermissions[index].value,
|
||||
onCheckedChange = { _ ->
|
||||
permission.checked = !permission.checked
|
||||
enabledPermissions[index].value = permission.checked
|
||||
item.checked = !item.checked
|
||||
enabledPermissions[index].value = item.checked
|
||||
},
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = permission.toLocalizedString(LocalContext.current),
|
||||
text = item.toLocalizedString(LocalContext.current),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun IconRow(
|
||||
modifier: Modifier = Modifier,
|
||||
center: Boolean = false,
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
@@ -31,7 +32,7 @@ fun IconRow(
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
|
||||
+5
-8
@@ -70,24 +70,21 @@ fun IntentSingleEventHomeScreen(
|
||||
when (intentData.type) {
|
||||
SignerType.GET_PUBLIC_KEY -> {
|
||||
LoginWithPubKey(
|
||||
shouldCloseApp = applicationEntity?.application?.closeApplication != false,
|
||||
packageName = packageName,
|
||||
modifier = modifier,
|
||||
account = account,
|
||||
packageName = packageName,
|
||||
appName = appName,
|
||||
applicationName = applicationName,
|
||||
permissions = intentData.permissions,
|
||||
onAccept = { permissions, signPolicy, closeApplication, rememberType ->
|
||||
onAccept = { permissions, signPolicy, closeApplication, rememberType, acc ->
|
||||
val result = if (packageName == null) {
|
||||
account.hexKey
|
||||
acc.hexKey
|
||||
} else {
|
||||
account.npub
|
||||
acc.npub
|
||||
}
|
||||
|
||||
IntentUtils.sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
acc,
|
||||
key,
|
||||
clipboardManager,
|
||||
result,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -11,15 +13,26 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
@@ -31,15 +44,22 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
|
||||
import coil3.compose.AsyncImage
|
||||
import com.greenart7c3.nostrsigner.Amber
|
||||
import com.greenart7c3.nostrsigner.BuildFlavorChecker
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.Permission
|
||||
@@ -94,16 +114,44 @@ fun ProfilePicture(account: Account) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfilePictureIcon(account: Account) {
|
||||
val profileUrl by account.picture.collectAsStateWithLifecycle()
|
||||
if (profileUrl.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) {
|
||||
AsyncImage(
|
||||
profileUrl,
|
||||
Route.Accounts.route,
|
||||
Modifier
|
||||
.clip(
|
||||
RoundedCornerShape(50),
|
||||
)
|
||||
.height(40.dp)
|
||||
.width(40.dp),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Outlined.Person,
|
||||
Route.Accounts.route,
|
||||
modifier = Modifier
|
||||
.border(
|
||||
2.dp,
|
||||
Color.fromHex(account.hexKey.slice(0..5)),
|
||||
CircleShape,
|
||||
)
|
||||
.height(40.dp)
|
||||
.width(40.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginWithPubKey(
|
||||
shouldCloseApp: Boolean,
|
||||
modifier: Modifier,
|
||||
account: Account,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
applicationName: String?,
|
||||
permissions: List<Permission>?,
|
||||
onAccept: (List<Permission>?, Int, Boolean?, RememberType) -> Unit,
|
||||
onAccept: (List<Permission>?, Int, Boolean?, RememberType, Account) -> Unit,
|
||||
onReject: (RememberType) -> Unit,
|
||||
) {
|
||||
val localPermissions = remember {
|
||||
@@ -115,105 +163,232 @@ fun LoginWithPubKey(
|
||||
}
|
||||
|
||||
var rememberType by remember { mutableStateOf(RememberType.NEVER) }
|
||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||
var selectedOption by remember { mutableIntStateOf(account.signPolicy) }
|
||||
val accounts = remember {
|
||||
val snapshot = mutableStateListOf<Account>()
|
||||
LocalPreferences.allAccounts(Amber.instance).forEach {
|
||||
snapshot.add(it)
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
var selectedAccountIndex by remember { mutableIntStateOf(accounts.indexOf(account)) }
|
||||
|
||||
Column(
|
||||
modifier,
|
||||
) {
|
||||
ProfilePicture(account)
|
||||
|
||||
packageName?.let {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
val appDisplayInfo = rememberAppDisplayInfo(packageName)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (appDisplayInfo.icon != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(MaterialTheme.shapes.small),
|
||||
bitmap = appDisplayInfo.icon.toBitmap().asImageBitmap(),
|
||||
contentDescription = appDisplayInfo.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = appDisplayInfo.name,
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
),
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = packageName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = applicationName ?: appName,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
indicator = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.tabIndicatorOffset(selectedTabIndex)
|
||||
.height(6.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
},
|
||||
divider = {
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
},
|
||||
) {
|
||||
SignerConnectAppTab(
|
||||
text = stringResource(id = R.string.login).uppercase(),
|
||||
selected = selectedTabIndex == 0,
|
||||
onClick = {
|
||||
selectedTabIndex = 0
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf),
|
||||
)
|
||||
|
||||
var selectedOption by remember { mutableIntStateOf(account.signPolicy) }
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.handle_application_permissions),
|
||||
)
|
||||
SignerConnectAppTab(
|
||||
text = stringResource(id = R.string.permissions).uppercase(),
|
||||
selected = selectedTabIndex == 0,
|
||||
onClick = {
|
||||
selectedTabIndex = 1
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
var closeApp by remember { mutableStateOf(shouldCloseApp) }
|
||||
if (packageName == null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable {
|
||||
closeApp = !closeApp
|
||||
if (selectedTabIndex == 0) {
|
||||
accounts.forEachIndexed { index, acc ->
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (selectedAccountIndex == index) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(4.dp)
|
||||
.selectable(
|
||||
selected = selectedAccountIndex == index,
|
||||
onClick = {
|
||||
selectedAccountIndex = index
|
||||
},
|
||||
),
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
|
||||
),
|
||||
leadingContent = {
|
||||
ProfilePictureIcon(
|
||||
account = acc,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.close_application),
|
||||
)
|
||||
Switch(
|
||||
checked = closeApp,
|
||||
onCheckedChange = {
|
||||
closeApp = it
|
||||
headlineContent = {
|
||||
val name by acc.name.collectAsStateWithLifecycle()
|
||||
Text(
|
||||
name.ifBlank { acc.npub.toShortenHex() },
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
var showModal by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
)
|
||||
|
||||
ChooseSignPolicy(
|
||||
selectedOption = selectedOption,
|
||||
onSelected = {
|
||||
selectedOption = it
|
||||
},
|
||||
)
|
||||
|
||||
if (selectedOption == 1 && localPermissions.isNotEmpty()) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ElevatedButton(
|
||||
colors = ButtonDefaults.buttonColors().copy(
|
||||
contentColor = Color.Black,
|
||||
),
|
||||
shape = RoundedCornerShape(20),
|
||||
content = {
|
||||
Text(stringResource(R.string.permissions))
|
||||
},
|
||||
onClick = {
|
||||
showModal = true
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showModal) {
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
showModal = false
|
||||
},
|
||||
) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomAppBar {
|
||||
IconRow(
|
||||
center = true,
|
||||
title = stringResource(R.string.go_back),
|
||||
icon = ImageVector.vectorResource(R.drawable.back),
|
||||
onClick = {
|
||||
showModal = false
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
EnabledPermissions(
|
||||
Modifier.padding(it),
|
||||
localPermissions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChooseSignPolicy(
|
||||
selectedOption = selectedOption,
|
||||
onSelected = {
|
||||
selectedOption = it
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Column(
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally,
|
||||
Arrangement.spacedBy(8.dp),
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
if (selectedOption == 1) {
|
||||
EnabledPermissions(localPermissions)
|
||||
}
|
||||
|
||||
AmberButton(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
onClick = {
|
||||
onAccept(localPermissions, selectedOption, closeApp, rememberType)
|
||||
},
|
||||
text = stringResource(R.string.grant_permissions),
|
||||
)
|
||||
|
||||
AmberButton(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 20.dp)
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
onReject(rememberType)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFFF6B00),
|
||||
),
|
||||
text = stringResource(R.string.reject),
|
||||
text = stringResource(R.string.cancel),
|
||||
)
|
||||
|
||||
AmberButton(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 20.dp)
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
onAccept(localPermissions, selectedOption, true, rememberType, accounts[selectedAccountIndex])
|
||||
},
|
||||
text = stringResource(R.string.connect),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SignerConnectAppTab(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Tab(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -347,7 +347,7 @@
|
||||
<string name="next">Next</string>
|
||||
<string name="add_your_key">Add your key</string>
|
||||
<string name="sign_policy_basic">Approve basic actions</string>
|
||||
<string name="sign_policy_basic_explainer">Recommended for most people. This will minimize the number of interruptions during your app usage.</string>
|
||||
<string name="sign_policy_basic_explainer">Auto-approve most common requests.</string>
|
||||
<string name="sign_policy_manual">Manually approve each app</string>
|
||||
<string name="sign_policy_manual_explainer">Recommended for privacy-minded people who would like control over each app. Choosing this will prompt you set a preference every time you try a new app.</string>
|
||||
<string name="sign_policy_title">Select a signing policy</string>
|
||||
@@ -359,7 +359,7 @@
|
||||
<string name="sign_policy">Sign policy</string>
|
||||
<string name="handle_application_permissions">How to handle this application permissions?</string>
|
||||
<string name="sign_policy_manual_new_app">Manually approve each permission</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Recommended for privacy-minded people who would like control over each permission. Choosing this will prompt you set a preference every time you receive a new permission.</string>
|
||||
<string name="sign_policy_manual_new_app_explainer">Ask me to approve each request.</string>
|
||||
<string name="sign_policy_fully">I fully trust this application</string>
|
||||
<string name="sign_policy_fully_explainer">Sign automatically every request</string>
|
||||
<string name="event_kind_10050">DM relays</string>
|
||||
|
||||
Reference in New Issue
Block a user