From 77a3ea4d1a36a902c76e36251a8dc1c90952ac37 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Sep 2023 10:45:58 -0300 Subject: [PATCH] use material 3, created permissions and settings screen, added more build targets --- .github/workflows/create-release.yml | 22 + app/build.gradle | 8 +- app/src/main/AndroidManifest.xml | 1 + .../nostrsigner/LocalPreferences.kt | 1 + .../greenart7c3/nostrsigner/MainActivity.kt | 6 +- .../greenart7c3/nostrsigner/models/Account.kt | 2 +- .../nostrsigner/ui/AccountScreen.kt | 5 +- .../nostrsigner/ui/AccountState.kt | 2 +- .../nostrsigner/ui/AccountStateViewModel.kt | 24 +- .../greenart7c3/nostrsigner/ui/HomeScreen.kt | 243 ++++++ .../nostrsigner/ui/LifeCycleUtils.kt | 27 + .../greenart7c3/nostrsigner/ui/LoginScreen.kt | 35 +- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 698 +++--------------- .../nostrsigner/ui/PermissionsScreen.kt | 246 ++++++ .../nostrsigner/ui/QrCodeDrawer.kt | 6 +- .../nostrsigner/ui/SettingsScreen.kt | 101 +++ .../nostrsigner/ui/ShowQRDialog.kt | 6 +- .../ui/actions/AccountBackupDialog.kt | 31 +- .../ui/components/AcceptRejectButtons.kt | 41 + .../nostrsigner/ui/components/AppTitle.kt | 18 + .../nostrsigner/ui/components/CloseButton.kt | 10 +- .../nostrsigner/ui/components/CloseIcon.kt | 10 +- .../nostrsigner/ui/components/Drawer.kt | 42 +- .../ui/components/EncryptDecryptData.kt | 69 ++ .../nostrsigner/ui/components/EventData.kt | 122 +++ .../nostrsigner/ui/components/EventRow.kt | 53 ++ .../nostrsigner/ui/components/IconRow.kt | 4 +- .../ui/components/LoginWithPubKey.kt | 62 ++ .../nostrsigner/ui/components/PostButton.kt | 10 +- .../nostrsigner/ui/components/RawJson.kt | 58 ++ .../ui/components/RawJsonButton.kt | 32 + .../ui/components/RememberMyChoice.kt | 47 ++ .../nostrsigner/ui/components/TopAppBar.kt | 104 --- .../nostrsigner/ui/navigation/Route.kt | 36 + .../ui/previews/EventDataPreview.kt | 31 +- .../greenart7c3/nostrsigner/ui/theme/Theme.kt | 28 +- .../greenart7c3/nostrsigner/ui/theme/Type.kt | 4 +- build.gradle | 4 +- 38 files changed, 1399 insertions(+), 850 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/LifeCycleUtils.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/PermissionsScreen.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AppTitle.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventRow.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt delete mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TopAppBar.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index d07395f4..066d7f6f 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 87b162fd..9416a31f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b636a52c..ff86947f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:supportsRtl="true" android:theme="@style/Theme.SignerSplash" android:usesCleartextTraffic="true" + android:enableOnBackInvokedCallback="true" tools:targetApi="33"> + var savedApps: MutableMap ) { val saveable: AccountLiveData = AccountLiveData(this) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt index 1d57413d..c5cdaa0d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountScreen.kt @@ -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) } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountState.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountState.kt index 94863cd6..74b4e92d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountState.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountState.kt @@ -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() } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index 4ab50a2f..9b18a66d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -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) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt new file mode 100644 index 00000000..e953771b --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/HomeScreen.kt @@ -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() + } + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LifeCycleUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LifeCycleUtils.kt new file mode 100644 index 00000000..58eab8fc --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LifeCycleUtils.kt @@ -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 +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt index ac272be4..7555ab19 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt @@ -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 ) ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 00b6b3f0..285e3182 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -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, - 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, - 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, - 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, - 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() - ) - } -} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/PermissionsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/PermissionsScreen.kt new file mode 100644 index 00000000..0a8f5498 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/PermissionsScreen.kt @@ -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(null) } + var permissions by remember { mutableStateOf(listOf()) } + 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) + } + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/QrCodeDrawer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/QrCodeDrawer.kt index 70cd2b53..60273ce8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/QrCodeDrawer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/QrCodeDrawer.kt @@ -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 diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt new file mode 100644 index 00000000..8214a06c --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt @@ -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 }) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ShowQRDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ShowQRDialog.kt index f50a4c40..e5503d86 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ShowQRDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ShowQRDialog.kt @@ -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( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt index c527ed54..45548c69 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt @@ -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 ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt new file mode 100644 index 00000000..fbcbf8d1 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AcceptRejectButtons.kt @@ -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") + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AppTitle.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AppTitle.kt new file mode 100644 index 00000000..22ceb8cd --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AppTitle.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseButton.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseButton.kt index 79478b85..91d76322 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseButton.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseButton.kt @@ -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() } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseIcon.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseIcon.kt index 0f3239b3..8a54d482 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseIcon.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/CloseIcon.kt @@ -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 ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Drawer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Drawer.kt index 01ba717b..aecf352c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Drawer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/Drawer.kt @@ -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 ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt new file mode 100644 index 00000000..957f927e --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EncryptDecryptData.kt @@ -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, + 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 + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt new file mode 100644 index 00000000..5a62a69d --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt @@ -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, + 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 + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventRow.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventRow.kt new file mode 100644 index 00000000..261a4b48 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventRow.kt @@ -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() + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt index b0fee5f0..ff04feb7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt @@ -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 diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt new file mode 100644 index 00000000..a784fd4c --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt @@ -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, + 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 + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt index 011bb523..411152f7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/PostButton.kt @@ -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) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt new file mode 100644 index 00000000..43500f38 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJson.kt @@ -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") + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt new file mode 100644 index 00000000..da8753bc --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RawJsonButton.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt new file mode 100644 index 00000000..3babd586 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RememberMyChoice.kt @@ -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, + 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 + } + ) + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TopAppBar.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TopAppBar.kt deleted file mode 100644 index f4a14bde..00000000 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TopAppBar.kt +++ /dev/null @@ -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 - ) - } -} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt new file mode 100644 index 00000000..afb9c60e --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt index 0ee03799..cf99d160 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt @@ -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, + { }, + { } + ) + } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt index 42392cd9..e7113abe 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Theme.kt @@ -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 diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Type.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Type.kt index 116d4aab..ac6fb8fd 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Type.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/theme/Type.kt @@ -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 diff --git a/build.gradle b/build.gradle index 9d06c1b0..ce5f72d2 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } }