mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
use material 3, created permissions and settings screen, added more
build targets
This commit is contained in:
@@ -93,6 +93,28 @@ jobs:
|
||||
asset_name: amber-x86_64-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload APK arm64-v8a Asset
|
||||
id: upload-release-asset-arm64-v8a-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/release/app-play-arm64-v8a-release-unsigned-signed.apk
|
||||
asset_name: amber-arm64-v8a-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload APK armeabi-v7a Asset
|
||||
id: upload-release-asset-armeabi-v7a-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/release/app-play-armeabi-v7a-release-unsigned-signed.apk
|
||||
asset_name: amber-armeabi-v7a-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload AAB Asset
|
||||
id: upload-release-asset-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ android {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include "x86", "x86_64"
|
||||
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ dependencies {
|
||||
implementation 'net.java.dev.jna:jna:5.13.0@aar'
|
||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.activity:activity-compose:1.7.2'
|
||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
|
||||
@@ -87,7 +87,7 @@ dependencies {
|
||||
// Observe Live data as State
|
||||
implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version"
|
||||
|
||||
implementation "androidx.compose.material:material:$compose_ui_version"
|
||||
implementation "androidx.compose.material3:material3:1.1.1"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_ui_version"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
@@ -119,7 +119,7 @@ dependencies {
|
||||
|
||||
// Markdown (With fix for full-image bleeds)
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material3:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63')
|
||||
|
||||
// Biometrics
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SignerSplash"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="33">
|
||||
<provider
|
||||
android:name=".SignerProvider"
|
||||
|
||||
@@ -157,6 +157,7 @@ object LocalPreferences {
|
||||
prefs.edit().apply {
|
||||
remove(PrefKeys.REMEMBER_APPS)
|
||||
}.apply()
|
||||
account.savedApps = mutableMapOf()
|
||||
}
|
||||
|
||||
fun saveToEncryptedStorage(account: Account) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -24,7 +24,7 @@ class MainActivity : AppCompatActivity() {
|
||||
NostrSignerTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel {
|
||||
AccountStateViewModel()
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.greenart7c3.nostrsigner.service.KeyPair
|
||||
@Stable
|
||||
class Account(
|
||||
val keyPair: KeyPair,
|
||||
val savedApps: MutableMap<String, Boolean>
|
||||
var savedApps: MutableMap<String, Boolean>
|
||||
) {
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ fun AccountScreen(
|
||||
Column {
|
||||
Crossfade(
|
||||
targetState = accountState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
label = "AccountScreen"
|
||||
) { state ->
|
||||
when (state) {
|
||||
is AccountState.LoggedOff -> {
|
||||
@@ -29,7 +30,7 @@ fun AccountScreen(
|
||||
is AccountState.LoggedIn -> {
|
||||
val intentData = IntentUtils.getIntentData(intent)
|
||||
|
||||
MainScreen(state.account, accountStateViewModel, intentData, packageName)
|
||||
MainScreen(state.account, accountStateViewModel, intentData, packageName, state.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import com.greenart7c3.nostrsigner.models.Account
|
||||
|
||||
sealed class AccountState {
|
||||
object LoggedOff : AccountState()
|
||||
class LoggedIn(val account: Account) : AccountState()
|
||||
class LoggedIn(val account: Account, val route: String?) : AccountState()
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ class AccountStateViewModel : ViewModel() {
|
||||
val accountContent = _accountContent.asStateFlow()
|
||||
|
||||
init {
|
||||
tryLoginExistingAccount()
|
||||
tryLoginExistingAccount(null)
|
||||
}
|
||||
|
||||
private fun tryLoginExistingAccount() {
|
||||
private fun tryLoginExistingAccount(route: String?) {
|
||||
LocalPreferences.loadFromEncryptedStorage()?.let {
|
||||
startUI(it)
|
||||
startUI(it, route)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,16 @@ class AccountStateViewModel : ViewModel() {
|
||||
fun logOff(npub: String) {
|
||||
prepareLogoutOrSwitch()
|
||||
LocalPreferences.updatePrefsForLogout(npub)
|
||||
tryLoginExistingAccount()
|
||||
tryLoginExistingAccount(null)
|
||||
}
|
||||
|
||||
fun startUI(key: String) {
|
||||
fun switchUser(npub: String, route: String?) {
|
||||
prepareLogoutOrSwitch()
|
||||
LocalPreferences.switchToAccount(npub)
|
||||
tryLoginExistingAccount(route)
|
||||
}
|
||||
|
||||
fun startUI(key: String, route: String?) {
|
||||
val account = if (key.startsWith("nsec")) {
|
||||
Account(KeyPair(privKey = key.bechToBytes()), mutableMapOf())
|
||||
} else {
|
||||
@@ -58,18 +64,18 @@ class AccountStateViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account)
|
||||
startUI(account, route)
|
||||
}
|
||||
|
||||
fun newKey() {
|
||||
val account = Account(KeyPair(), mutableMapOf())
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account)
|
||||
startUI(account, null)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun startUI(account: Account) {
|
||||
_accountContent.update { AccountState.LoggedIn(account) }
|
||||
fun startUI(account: Account, route: String?) {
|
||||
_accountContent.update { AccountState.LoggedIn(account, route) }
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
account.saveable.observeForever(saveListener)
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.service.AmberUtils
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
|
||||
import com.greenart7c3.nostrsigner.ui.components.EncryptDecryptData
|
||||
import com.greenart7c3.nostrsigner.ui.components.EventData
|
||||
import com.greenart7c3.nostrsigner.ui.components.LoginWithPubKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
modifier: Modifier,
|
||||
json: IntentData?,
|
||||
packageName: String?,
|
||||
account: Account
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
Box(
|
||||
modifier
|
||||
) {
|
||||
if (json == null) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("No event to sign")
|
||||
}
|
||||
} else {
|
||||
json.let {
|
||||
var key = "$packageName-${it.type}"
|
||||
val appName = packageName ?: it.name
|
||||
when (it.type) {
|
||||
SignerType.GET_PUBLIC_KEY -> {
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
LoginWithPubKey(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
{
|
||||
val sig = account.keyPair.pubKey.toNpub()
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
"",
|
||||
"",
|
||||
sig
|
||||
)
|
||||
}
|
||||
return@LoginWithPubKey
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, SignerType.DECRYPT_ZAP_EVENT -> {
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
EncryptDecryptData(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
it.type,
|
||||
{
|
||||
try {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val sig = try {
|
||||
AmberUtils.encryptOrDecryptData(
|
||||
it.data,
|
||||
it.type,
|
||||
account,
|
||||
it.pubKey
|
||||
)
|
||||
?: context.getString(R.string.could_not_decrypt_the_message)
|
||||
} catch (e: Exception) {
|
||||
context.getString(R.string.could_not_decrypt_the_message)
|
||||
}
|
||||
|
||||
val result =
|
||||
if (sig == context.getString(R.string.could_not_decrypt_the_message) && (it.type == SignerType.DECRYPT_ZAP_EVENT)) {
|
||||
""
|
||||
} else {
|
||||
sig
|
||||
}
|
||||
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
"",
|
||||
it.id,
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
return@EncryptDecryptData
|
||||
} catch (e: Exception) {
|
||||
val message = if (it.type.toString().contains("ENCRYPT", true)) {
|
||||
"encrypt"
|
||||
} else {
|
||||
"decrypt"
|
||||
}
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Error to $message data",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@EncryptDecryptData
|
||||
}
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val event =
|
||||
IntentUtils.getIntent(it.data, account.keyPair)
|
||||
key = "$packageName-${it.type}-${event.kind}"
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
EventData(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
event,
|
||||
event.toJson(),
|
||||
{
|
||||
if (event.pubKey != account.keyPair.pubKey.toHexKey()) {
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@EventData
|
||||
}
|
||||
|
||||
val localEvent =
|
||||
Event.fromJson(
|
||||
it.data
|
||||
)
|
||||
if (localEvent is LnZapRequestEvent && localEvent.tags.any { tag -> tag.any { t -> t == "anon" } }) {
|
||||
val resultEvent =
|
||||
AmberUtils.getZapRequestEvent(
|
||||
localEvent,
|
||||
account.keyPair.privKey
|
||||
)
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
resultEvent.toJson(),
|
||||
it.id,
|
||||
resultEvent.toJson()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val signedEvent = AmberUtils.getSignedEvent(
|
||||
event,
|
||||
account.keyPair.privKey
|
||||
)
|
||||
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
signedEvent.toJson(),
|
||||
it.id,
|
||||
signedEvent.sig
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
@Composable
|
||||
fun rememberLifecycleEvent(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): Lifecycle.Event {
|
||||
var state by remember { mutableStateOf(Lifecycle.Event.ON_ANY) }
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
state = event
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
@@ -17,16 +17,15 @@ import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -56,7 +55,6 @@ import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.ui.theme.Font14SP
|
||||
import com.greenart7c3.nostrsigner.ui.theme.Size35dp
|
||||
import com.greenart7c3.nostrsigner.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
@@ -122,8 +120,7 @@ fun LoginPage(
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "nsec..",
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
text = "nsec.."
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
@@ -154,7 +151,7 @@ fun LoginPage(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -162,7 +159,7 @@ fun LoginPage(
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = {
|
||||
try {
|
||||
accountViewModel.startUI(key.value.text)
|
||||
accountViewModel.startUI(key.value.text, null)
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Invalid key"
|
||||
}
|
||||
@@ -172,8 +169,8 @@ fun LoginPage(
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colors.error,
|
||||
style = MaterialTheme.typography.caption
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,7 +185,7 @@ fun LoginPage(
|
||||
|
||||
if (key.value.text.isNotBlank()) {
|
||||
try {
|
||||
accountViewModel.startUI(key.value.text)
|
||||
accountViewModel.startUI(key.value.text, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = "Invalid key"
|
||||
@@ -198,11 +195,7 @@ fun LoginPage(
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
.height(50.dp)
|
||||
) {
|
||||
Text(text = "Login")
|
||||
}
|
||||
@@ -221,7 +214,7 @@ fun LoginPage(
|
||||
style = TextStyle(
|
||||
fontSize = Font14SP,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colors.primary,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,76 +4,35 @@ import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
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
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DrawerValue
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccessTime
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.rememberDrawerState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.AmberUtils
|
||||
import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
|
||||
import com.greenart7c3.nostrsigner.service.model.Event
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.components.Drawer
|
||||
import com.greenart7c3.nostrsigner.ui.components.MainAppBar
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
fun sendResult(
|
||||
context: Context,
|
||||
@@ -88,7 +47,9 @@ fun sendResult(
|
||||
) {
|
||||
val activity = context.getAppCompatActivity()
|
||||
if (packageName != null) {
|
||||
account.savedApps[key] = rememberChoice
|
||||
if (rememberChoice) {
|
||||
account.savedApps[key] = rememberChoice
|
||||
}
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
val intent = Intent()
|
||||
intent.putExtra("signature", value)
|
||||
@@ -106,568 +67,101 @@ fun sendResult(
|
||||
activity?.finish()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, json: IntentData?, packageName: String?) {
|
||||
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, json: IntentData?, packageName: String?, route: String?) {
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.Settings)
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
Drawer(
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
account = account,
|
||||
scaffoldState = scaffoldState
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text(text = "Amber") }
|
||||
)
|
||||
},
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
scaffoldState,
|
||||
accountStateViewModel,
|
||||
account
|
||||
)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) {
|
||||
if (json == null) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("No event to sign")
|
||||
}
|
||||
} else {
|
||||
json.let {
|
||||
var key = "$packageName-${it.type}"
|
||||
val appName = packageName ?: it.name
|
||||
when (it.type) {
|
||||
SignerType.GET_PUBLIC_KEY -> {
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
items.forEach {
|
||||
val selected = currentRoute == it.route
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(it.route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
LoginWithPubKey(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
{
|
||||
val sig = account.keyPair.pubKey.toNpub()
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
"",
|
||||
"",
|
||||
sig
|
||||
)
|
||||
}
|
||||
return@LoginWithPubKey
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
if (selected) it.selectedIcon else it.icon,
|
||||
it.route
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(it.route)
|
||||
}
|
||||
SignerType.NIP04_DECRYPT, SignerType.NIP04_ENCRYPT, SignerType.NIP44_ENCRYPT, SignerType.NIP44_DECRYPT, SignerType.DECRYPT_ZAP_EVENT -> {
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
EncryptDecryptData(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
it.type,
|
||||
{
|
||||
try {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val sig = try {
|
||||
AmberUtils.encryptOrDecryptData(
|
||||
it.data,
|
||||
it.type,
|
||||
account,
|
||||
it.pubKey
|
||||
) ?: context.getString(R.string.could_not_decrypt_the_message)
|
||||
} catch (e: Exception) {
|
||||
context.getString(R.string.could_not_decrypt_the_message)
|
||||
}
|
||||
|
||||
val result = if (sig == context.getString(R.string.could_not_decrypt_the_message) && (it.type == SignerType.DECRYPT_ZAP_EVENT)) {
|
||||
""
|
||||
} else {
|
||||
sig
|
||||
}
|
||||
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
"",
|
||||
it.id,
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
return@EncryptDecryptData
|
||||
} catch (e: Exception) {
|
||||
val message = if (it.type.toString().contains("ENCRYPT", true)) "encrypt" else "decrypt"
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Error to $message data",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@EncryptDecryptData
|
||||
}
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val event = IntentUtils.getIntent(it.data, account.keyPair)
|
||||
key = "$packageName-${it.type}-${event.kind}"
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
EventData(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
event,
|
||||
event.toJson(),
|
||||
{
|
||||
if (event.pubKey != account.keyPair.pubKey.toHexKey()) {
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@EventData
|
||||
}
|
||||
|
||||
val localEvent = com.vitorpamplona.quartz.events.Event.fromJson(it.data)
|
||||
if (localEvent is LnZapRequestEvent && localEvent.tags.any { tag -> tag.any { t -> t == "anon" } }) {
|
||||
val resultEvent = AmberUtils.getZapRequestEvent(localEvent, account.keyPair.privKey)
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
resultEvent.toJson(),
|
||||
it.id,
|
||||
resultEvent.toJson()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val signedEvent = AmberUtils.getSignedEvent(event, account.keyPair.privKey)
|
||||
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
packageName,
|
||||
account,
|
||||
key,
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
signedEvent.toJson(),
|
||||
it.id,
|
||||
signedEvent.sig
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppTitle(appName: String) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = appName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RememberMyChoice(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
onAccept: () -> Unit
|
||||
) {
|
||||
if (shouldRunOnAccept) {
|
||||
LaunchedEffect(Unit) {
|
||||
onAccept()
|
||||
}
|
||||
}
|
||||
if (packageName != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
remember.value = !remember.value
|
||||
}
|
||||
) { padding ->
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = Route.Home.route,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = "Remember my choice and don't ask again"
|
||||
composable(
|
||||
Route.Home.route,
|
||||
content = {
|
||||
HomeScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
json,
|
||||
packageName,
|
||||
account
|
||||
)
|
||||
}
|
||||
)
|
||||
Switch(
|
||||
checked = remember.value,
|
||||
onCheckedChange = {
|
||||
remember.value = !remember.value
|
||||
|
||||
composable(
|
||||
Route.Permissions.route,
|
||||
content = {
|
||||
PermissionsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
account = account,
|
||||
accountStateViewModel = accountStateViewModel
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Settings.route,
|
||||
content = {
|
||||
SettingsScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
accountStateViewModel,
|
||||
account
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AcceptRejectButtons(
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
Arrangement.Center
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onReject
|
||||
) {
|
||||
Text("Reject")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onAccept
|
||||
) {
|
||||
Text("Accept")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EncryptDecryptData(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
type: SignerType,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val message = when (type) {
|
||||
SignerType.NIP44_ENCRYPT -> "encrypt nip44"
|
||||
SignerType.NIP04_ENCRYPT -> "encrypt nip04"
|
||||
SignerType.NIP44_DECRYPT -> "decrypt nip44"
|
||||
SignerType.NIP04_DECRYPT -> "decrypt nip04"
|
||||
SignerType.DECRYPT_ZAP_EVENT -> "decrypt zap event"
|
||||
else -> "encrypt/decrypt"
|
||||
LaunchedEffect(Unit) {
|
||||
if (route !== null) {
|
||||
delay(200)
|
||||
try {
|
||||
navController.navigate(route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
Text(
|
||||
"wants you to $message data",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginWithPubKey(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(6.dp)
|
||||
) {
|
||||
Text(
|
||||
"wants to read your public key",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EventData(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
event: Event,
|
||||
rawJson: String,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
var showMore by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val eventDescription = event.description()
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val message = if (event.kind == 22242) {
|
||||
"client authentication"
|
||||
} else {
|
||||
eventDescription
|
||||
}
|
||||
Text(
|
||||
"wants you to sign a $message",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.CalendarMonth,
|
||||
"Kind ",
|
||||
"${event.kind}"
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.AccessTime,
|
||||
"Created at ",
|
||||
TimeUtils.format(event.createdAt)
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.Person,
|
||||
"Signed by ",
|
||||
event.pubKey.toShortenHex()
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.ContentPaste,
|
||||
"Content ",
|
||||
event.content
|
||||
)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 15.dp),
|
||||
thickness = Dp.Hairline
|
||||
)
|
||||
}
|
||||
}
|
||||
RawJsonButton(
|
||||
onCLick = {
|
||||
showMore = !showMore
|
||||
},
|
||||
if (!showMore) "Show Details" else "Hide Details"
|
||||
)
|
||||
if (showMore) {
|
||||
RawJson(rawJson, Modifier.weight(1f))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RawJsonButton(
|
||||
onCLick: () -> Unit,
|
||||
text: String
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onCLick
|
||||
) {
|
||||
Text(text)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RawJson(
|
||||
rawJson: String,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
value = TextFieldValue(JSONObject(rawJson).toString(2)),
|
||||
onValueChange = { },
|
||||
readOnly = true
|
||||
)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(rawJson))
|
||||
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.raw_json_copied_to_the_clipboard),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Copy raw json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EventRow(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
content: String
|
||||
) {
|
||||
Divider(
|
||||
modifier = Modifier.padding(vertical = 15.dp),
|
||||
thickness = Dp.Hairline
|
||||
)
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = "$title ",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
content,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.ui.components.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.components.IconRow
|
||||
import com.greenart7c3.nostrsigner.ui.components.PostButton
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun PermissionsScreen(
|
||||
modifier: Modifier,
|
||||
account: Account,
|
||||
accountStateViewModel: AccountStateViewModel
|
||||
) {
|
||||
val lifecycleEvent = rememberLifecycleEvent()
|
||||
|
||||
val localAccount = LocalPreferences.loadFromEncryptedStorage(account.keyPair.pubKey.toNpub())
|
||||
val savedApps = localAccount!!.savedApps
|
||||
val applications = savedApps.keys.toList().map {
|
||||
it.split("-").first()
|
||||
}.toSet()
|
||||
var resetPermissions by remember { mutableStateOf(false) }
|
||||
var selectedPackage by remember { mutableStateOf<String?>(null) }
|
||||
var permissions by remember { mutableStateOf(listOf<String>()) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(lifecycleEvent) {
|
||||
if (lifecycleEvent == Lifecycle.Event.ON_RESUME) {
|
||||
selectedPackage = null
|
||||
}
|
||||
}
|
||||
|
||||
if (resetPermissions) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.reset_permissions))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.do_you_want_to_reset_all_permissions))
|
||||
},
|
||||
onDismissRequest = {
|
||||
resetPermissions = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
resetPermissions = false
|
||||
scope.launch {
|
||||
LocalPreferences.deleteSavedApps(localAccount)
|
||||
accountStateViewModel.switchUser(localAccount.keyPair.pubKey.toNpub(), Route.Permissions.route)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
resetPermissions = false
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedPackage !== null) {
|
||||
Dialog(
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
onDismissRequest = {
|
||||
selectedPackage = null
|
||||
}
|
||||
) {
|
||||
Surface(
|
||||
Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(
|
||||
onCancel = {
|
||||
selectedPackage = null
|
||||
}
|
||||
)
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch {
|
||||
val localSaved = localAccount.savedApps.filter { !it.key.contains(selectedPackage!!) }.toMutableMap()
|
||||
permissions.forEach {
|
||||
localSaved["$selectedPackage-$it"] = true
|
||||
}
|
||||
localAccount.savedApps = localSaved
|
||||
LocalPreferences.saveToEncryptedStorage(localAccount)
|
||||
selectedPackage = null
|
||||
accountStateViewModel.switchUser(localAccount.keyPair.pubKey.toNpub(), Route.Permissions.route)
|
||||
}
|
||||
},
|
||||
isActive = true
|
||||
)
|
||||
}
|
||||
|
||||
permissions.forEachIndexed { index, permission ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = permission,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(22.dp)
|
||||
.clickable {
|
||||
permissions =
|
||||
permissions.filterIndexed { i, _ -> i != index }
|
||||
},
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
IconRow(
|
||||
title = "Reset permissions",
|
||||
icon = Icons.Default.ClearAll,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
resetPermissions = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
if (applications.isEmpty()) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("No permissions granted")
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
items(applications.size) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
IconRow(
|
||||
title = applications.elementAt(it),
|
||||
icon = Icons.Default.ArrowForward,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
permissions = localAccount.savedApps.keys.filter { item -> item.startsWith(applications.elementAt(it)) }.map { item ->
|
||||
item.replace("${applications.elementAt(it)}-", "")
|
||||
}
|
||||
selectedPackage = applications.elementAt(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -34,13 +34,13 @@ fun QrCodeDrawer(contents: String, modifier: Modifier = Modifier) {
|
||||
createQrCode(contents = contents)
|
||||
}
|
||||
|
||||
val foregroundColor = MaterialTheme.colors.onSurface
|
||||
val foregroundColor = MaterialTheme.colorScheme.onSurface
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(48.dp, 48.dp)
|
||||
.aspectRatio(1f)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
// Calculate the height and width of each column/row
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExitToApp
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupDialog
|
||||
import com.greenart7c3.nostrsigner.ui.components.IconRow
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
modifier: Modifier,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
account: Account
|
||||
) {
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
if (logoutDialog) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.logout))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.logging_out_deletes_all_your_local_information_make_sure_to_have_your_private_keys_backed_up_to_avoid_losing_your_account_do_you_want_to_continue))
|
||||
},
|
||||
onDismissRequest = {
|
||||
logoutDialog = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
accountStateViewModel.logOff(account.keyPair.pubKey.toNpub())
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.logout))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
IconRow(
|
||||
title = "Backup Keys",
|
||||
icon = Icons.Default.Key,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
backupDialogOpen = true
|
||||
}
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
IconRow(
|
||||
title = "Logout",
|
||||
icon = Icons.Default.ExitToApp,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
logoutDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -29,7 +29,7 @@ fun ShowQRDialog(account: Account, onClose: () -> Unit) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Row(
|
||||
|
||||
@@ -16,14 +16,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -43,7 +42,7 @@ import com.greenart7c3.nostrsigner.ui.components.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.encoders.toNsec
|
||||
@@ -59,7 +58,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Row(
|
||||
@@ -79,7 +78,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
MaterialRichText(
|
||||
Material3RichText(
|
||||
style = RichTextStyle().resolveDefaults()
|
||||
) {
|
||||
Markdown(
|
||||
@@ -125,18 +124,15 @@ private fun NSecCopyButton(
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.onPrimary,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(stringResource(id = R.string.copy_my_secret_key), color = MaterialTheme.colors.onPrimary)
|
||||
Text(stringResource(id = R.string.copy_my_secret_key), color = MaterialTheme.colorScheme.onPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,20 +157,17 @@ private fun NPubCopyButton(
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.onPrimary,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription = stringResource(R.string.copies_the_npub_id_your_user_to_the_clipboard_for_backup),
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.copy_my_public_key),
|
||||
color = MaterialTheme.colors.onPrimary
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
|
||||
@Composable
|
||||
fun AcceptRejectButtons(
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
Arrangement.Center
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onReject
|
||||
) {
|
||||
Text("Reject")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onAccept
|
||||
) {
|
||||
Text("Accept")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.text.style.TextAlign
|
||||
|
||||
@Composable
|
||||
fun AppTitle(appName: String) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = appName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
|
||||
@Composable
|
||||
@@ -12,11 +10,7 @@ fun CloseButton(onCancel: () -> Unit) {
|
||||
onClick = {
|
||||
onCancel()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
shape = ButtonBorder
|
||||
) {
|
||||
CloseIcon()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.greenart7c3.nostrsigner.ui.theme.Size20Modifier
|
||||
|
||||
@Composable
|
||||
fun CloseIcon() {
|
||||
Icon(
|
||||
Icons.Default.Cancel,
|
||||
Icons.Outlined.Close,
|
||||
contentDescription = "Cancel",
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.White
|
||||
modifier = Size20Modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -46,7 +46,7 @@ import kotlinx.coroutines.launch
|
||||
fun Drawer(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
account: Account,
|
||||
scaffoldState: ScaffoldState
|
||||
drawerState: DrawerState
|
||||
) {
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
var resetPermissions by remember { mutableStateOf(false) }
|
||||
@@ -102,7 +102,7 @@ fun Drawer(
|
||||
onClick = {
|
||||
resetPermissions = false
|
||||
scope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
LocalPreferences.deleteSavedApps(account)
|
||||
}
|
||||
}
|
||||
@@ -124,14 +124,14 @@ fun Drawer(
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconRow(
|
||||
title = "Reset permissions",
|
||||
icon = Icons.Default.ClearAll,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
resetPermissions = true
|
||||
}
|
||||
@@ -139,27 +139,27 @@ fun Drawer(
|
||||
IconRow(
|
||||
title = "Backup Keys",
|
||||
icon = Icons.Default.Key,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
backupDialogOpen = true
|
||||
}
|
||||
)
|
||||
BottomContent(account, scaffoldState)
|
||||
BottomContent(account, drawerState)
|
||||
}
|
||||
}
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
BackHandler(enabled = scaffoldState.drawerState.isOpen) {
|
||||
scope.launch { scaffoldState.drawerState.close() }
|
||||
BackHandler(enabled = drawerState.isOpen) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomContent(account: Account, scaffoldState: ScaffoldState) {
|
||||
fun BottomContent(account: Account, drawerState: DrawerState) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
// store the dialog open or close state
|
||||
@@ -189,14 +189,14 @@ fun BottomContent(account: Account, scaffoldState: ScaffoldState) {
|
||||
IconButton(onClick = {
|
||||
dialogOpen = true
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
drawerState.close()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.QrCode,
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
|
||||
@Composable
|
||||
fun EncryptDecryptData(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
type: SignerType,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val message = when (type) {
|
||||
SignerType.NIP44_ENCRYPT -> "encrypt nip44"
|
||||
SignerType.NIP04_ENCRYPT -> "encrypt nip04"
|
||||
SignerType.NIP44_DECRYPT -> "decrypt nip44"
|
||||
SignerType.NIP04_DECRYPT -> "decrypt nip04"
|
||||
SignerType.DECRYPT_ZAP_EVENT -> "decrypt zap event"
|
||||
else -> "encrypt/decrypt"
|
||||
}
|
||||
Text(
|
||||
"wants you to $message data",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccessTime
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.Event
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
|
||||
@Composable
|
||||
fun EventData(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
event: Event,
|
||||
rawJson: String,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
var showMore by androidx.compose.runtime.remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val eventDescription = event.description()
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val message = if (event.kind == 22242) {
|
||||
"client authentication"
|
||||
} else {
|
||||
eventDescription
|
||||
}
|
||||
Text(
|
||||
"wants you to sign a $message",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.CalendarMonth,
|
||||
"Kind ",
|
||||
"${event.kind}"
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.AccessTime,
|
||||
"Created at ",
|
||||
TimeUtils.format(event.createdAt)
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.Person,
|
||||
"Signed by ",
|
||||
event.pubKey.toShortenHex()
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.ContentPaste,
|
||||
"Content ",
|
||||
event.content
|
||||
)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 15.dp),
|
||||
thickness = Dp.Hairline
|
||||
)
|
||||
}
|
||||
}
|
||||
RawJsonButton(
|
||||
onCLick = {
|
||||
showMore = !showMore
|
||||
},
|
||||
if (!showMore) "Show Details" else "Hide Details"
|
||||
)
|
||||
if (showMore) {
|
||||
RawJson(rawJson, Modifier.weight(1f))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
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.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun EventRow(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
content: String
|
||||
) {
|
||||
Divider(
|
||||
modifier = Modifier.padding(vertical = 15.dp),
|
||||
thickness = Dp.Hairline
|
||||
)
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = "$title ",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
content,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LoginWithPubKey(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(6.dp)
|
||||
) {
|
||||
Text(
|
||||
"wants to read your public key",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -21,10 +19,6 @@ fun PostButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive:
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text(text = "Post", color = Color.White)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.text.input.TextFieldValue
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
||||
@Composable
|
||||
fun RawJson(
|
||||
rawJson: String,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
value = TextFieldValue(JSONObject(rawJson).toString(2)),
|
||||
onValueChange = { },
|
||||
readOnly = true
|
||||
)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(rawJson))
|
||||
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.raw_json_copied_to_the_clipboard),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Copy raw json")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
|
||||
@Composable
|
||||
fun RawJsonButton(
|
||||
onCLick: () -> Unit,
|
||||
text: String
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onCLick
|
||||
) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun RememberMyChoice(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
onAccept: () -> Unit
|
||||
) {
|
||||
if (shouldRunOnAccept) {
|
||||
LaunchedEffect(Unit) {
|
||||
onAccept()
|
||||
}
|
||||
}
|
||||
if (packageName != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
remember.value = !remember.value
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = "Remember my choice and don't ask again"
|
||||
)
|
||||
Switch(
|
||||
checked = remember.value,
|
||||
onCheckedChange = {
|
||||
remember.value = !remember.value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MainAppBar(scaffoldState: ScaffoldState, accountStateViewModel: AccountStateViewModel, account: Account) {
|
||||
val scope = rememberCoroutineScope()
|
||||
TopAppBar(
|
||||
title = { Text(text = "Amber") },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
scaffoldState.drawerState.open()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
LogoutButton(
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
account = account
|
||||
)
|
||||
},
|
||||
backgroundColor = Color.Transparent,
|
||||
elevation = 0.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogoutButton(accountStateViewModel: AccountStateViewModel, account: Account) {
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
if (logoutDialog) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = "Logout")
|
||||
},
|
||||
text = {
|
||||
Text(text = "Logging out deletes all your local information. Make sure to have your private keys backed up to avoid losing your account. Do you want to continue?")
|
||||
},
|
||||
onDismissRequest = {
|
||||
logoutDialog = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
accountStateViewModel.logOff(account.keyPair.pubKey.toNpub())
|
||||
}
|
||||
) {
|
||||
Text(text = "Logout")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
}
|
||||
) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
logoutDialog = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Logout,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.greenart7c3.nostrsigner.ui.navigation
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.ViewList
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.ViewList
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Immutable
|
||||
sealed class Route(
|
||||
val route: String,
|
||||
val icon: ImageVector,
|
||||
val selectedIcon: ImageVector
|
||||
) {
|
||||
data object Home : Route(
|
||||
route = "Home",
|
||||
icon = Icons.Outlined.Home,
|
||||
selectedIcon = Icons.Default.Home
|
||||
)
|
||||
|
||||
data object Permissions : Route(
|
||||
route = "Permissions",
|
||||
icon = Icons.Outlined.ViewList,
|
||||
selectedIcon = Icons.Default.ViewList
|
||||
)
|
||||
|
||||
data object Settings : Route(
|
||||
route = "Settings",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.greenart7c3.nostrsigner.ui.previews
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.Event
|
||||
import com.greenart7c3.nostrsigner.ui.EventData
|
||||
import com.greenart7c3.nostrsigner.ui.components.EventData
|
||||
import com.greenart7c3.nostrsigner.ui.theme.NostrSignerTheme
|
||||
|
||||
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
|
||||
@@ -21,16 +24,20 @@ fun EventDataPreview() {
|
||||
|
||||
NostrSignerTheme(darkTheme = true) {
|
||||
Scaffold {
|
||||
EventData(
|
||||
false,
|
||||
remember,
|
||||
null,
|
||||
"App",
|
||||
event,
|
||||
data,
|
||||
{ },
|
||||
{ }
|
||||
)
|
||||
Box(
|
||||
Modifier.padding(it)
|
||||
) {
|
||||
EventData(
|
||||
false,
|
||||
remember,
|
||||
null,
|
||||
"App",
|
||||
event,
|
||||
data,
|
||||
{ },
|
||||
{ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import android.app.Activity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -37,26 +36,21 @@ val Teal200 = Color(0xFF03DAC5)
|
||||
val ButtonBorder = RoundedCornerShape(20.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
private val DarkColorPalette = darkColorScheme(
|
||||
primary = Purple200,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200,
|
||||
secondaryVariant = Purple200
|
||||
secondary = Purple700,
|
||||
tertiary = Teal200
|
||||
)
|
||||
|
||||
private val LightColorPalette = lightColors(
|
||||
private val LightColorPalette = lightColorScheme(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200,
|
||||
secondaryVariant = Purple500
|
||||
secondary = Purple700,
|
||||
tertiary = Teal200
|
||||
)
|
||||
|
||||
private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
|
||||
val Colors.placeholderText: Color
|
||||
get() = if (isLight) LightPlaceholderText else DarkPlaceholderText
|
||||
|
||||
@Composable
|
||||
fun NostrSignerTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
@@ -65,7 +59,7 @@ fun NostrSignerTheme(
|
||||
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
|
||||
|
||||
MaterialTheme(
|
||||
colors = colors,
|
||||
colorScheme = colors,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.greenart7c3.nostrsigner.ui.theme
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -8,7 +8,7 @@ import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
body1 = TextStyle(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
buildscript {
|
||||
ext {
|
||||
lifecycle_version = '2.6.1'
|
||||
compose_ui_version = '1.5.0'
|
||||
nav_version = "2.7.0"
|
||||
compose_ui_version = '1.5.1'
|
||||
nav_version = "2.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user