mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 03:18:23 +00:00
new ui, use NostrSignerInternal from quartz
This commit is contained in:
@@ -4,11 +4,8 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.History
|
||||
import com.greenart7c3.nostrsigner.service.KeyPair
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
@@ -28,7 +25,6 @@ private object PrefKeys {
|
||||
const val NOSTR_PRIVKEY = "nostr_privkey"
|
||||
const val NOSTR_PUBKEY = "nostr_pubkey"
|
||||
const val REMEMBER_APPS = "remember_apps"
|
||||
const val HISTORY = "history"
|
||||
const val ACCOUNT_NAME = "account_name"
|
||||
}
|
||||
|
||||
@@ -184,13 +180,11 @@ object LocalPreferences {
|
||||
fun saveToEncryptedStorage(account: Account) {
|
||||
val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub())
|
||||
prefs.edit().apply {
|
||||
account.keyPair.privKey.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||
account.keyPair.privKey.let { putString(PrefKeys.NOSTR_PRIVKEY, it?.toHexKey()) }
|
||||
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
val jsonObject = JSONObject(account.savedApps.toMap())
|
||||
putString(PrefKeys.REMEMBER_APPS, jsonObject.toString())
|
||||
putString(PrefKeys.ACCOUNT_NAME, account.name)
|
||||
val historyObject = Gson().toJson(account.history)
|
||||
putString(PrefKeys.HISTORY, historyObject)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
@@ -210,14 +204,6 @@ object LocalPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadHistory(npub: String?): MutableList<History> {
|
||||
encryptedPreferences(npub).apply {
|
||||
val historyJson = getString(PrefKeys.HISTORY, "[]") ?: "[]"
|
||||
val listType = object : TypeToken<List<History>>() {}.type
|
||||
return Gson().fromJson<List<History>>(historyJson, listType).toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFromEncryptedStorage(npub: String?): Account? {
|
||||
encryptedPreferences(npub).apply {
|
||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
|
||||
@@ -236,15 +222,10 @@ object LocalPreferences {
|
||||
}
|
||||
val name = getString(PrefKeys.ACCOUNT_NAME, "") ?: ""
|
||||
|
||||
val historyJson = getString(PrefKeys.HISTORY, "[]") ?: "[]"
|
||||
val listType = object : TypeToken<List<History>>() {}.type
|
||||
val history = Gson().fromJson<List<History>>(historyJson, listType).toMutableList()
|
||||
|
||||
return Account(
|
||||
keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
name = name,
|
||||
savedApps = outputMap,
|
||||
history = history
|
||||
savedApps = outputMap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
setContent {
|
||||
val packageName = callingPackage
|
||||
val appName = if (packageName != null) {
|
||||
val info = applicationContext.packageManager.getApplicationInfo(packageName, 0)
|
||||
applicationContext.packageManager.getApplicationLabel(info).toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
NostrSignerTheme {
|
||||
Surface(
|
||||
@@ -31,7 +37,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel {
|
||||
AccountStateViewModel(intent.getStringExtra("current_user"))
|
||||
}
|
||||
AccountScreen(accountStateViewModel, intent, packageName)
|
||||
AccountScreen(accountStateViewModel, intent, packageName, appName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,10 @@ import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import com.greenart7c3.nostrsigner.models.History
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.AmberUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
|
||||
class SignerProvider : ContentProvider() {
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
|
||||
@@ -51,24 +47,19 @@ class SignerProvider : ContentProvider() {
|
||||
val isRemembered = account.savedApps[key] ?: false
|
||||
if (!isRemembered) return null
|
||||
|
||||
if (event is LnZapRequestEvent && event.tags.any { tag -> tag.any { t -> t == "anon" } }) {
|
||||
val signedEvent = AmberUtils.getZapRequestEvent(event, account.keyPair.privKey)
|
||||
var cursor: MatrixCursor? = null
|
||||
|
||||
val cursor = MatrixCursor(arrayOf("signature", "event"))
|
||||
cursor.addRow(arrayOf(signedEvent.sig, signedEvent.toJson()))
|
||||
return cursor
|
||||
account.signer.sign<Event>(event.createdAt, event.kind, event.tags, event.content) { signedEvent ->
|
||||
val localCursor = MatrixCursor(arrayOf("signature", "event")).also {
|
||||
it.addRow(arrayOf(signedEvent.sig, signedEvent.toJson()))
|
||||
}
|
||||
cursor = localCursor
|
||||
}
|
||||
|
||||
val signedEvent = AmberUtils.getSignedEvent(
|
||||
AmberEvent.fromJson(json),
|
||||
account.keyPair.privKey
|
||||
)
|
||||
while (cursor == null) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
account.history.add(History(packageName, "SIGN_EVENT", TimeUtils.now(), signedEvent.kind))
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
|
||||
val cursor = MatrixCursor(arrayOf("signature", "event"))
|
||||
cursor.addRow(arrayOf(signedEvent.sig, signedEvent.toJson()))
|
||||
return cursor
|
||||
}
|
||||
"content://$appId.NIP04_DECRYPT",
|
||||
@@ -106,7 +97,6 @@ class SignerProvider : ContentProvider() {
|
||||
"Could not decrypt the message"
|
||||
}
|
||||
|
||||
account.history.add(History(packageName, stringType, TimeUtils.now(), null))
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
|
||||
if (type == SignerType.NIP04_ENCRYPT && result == "Could not decrypt the message") {
|
||||
@@ -125,7 +115,6 @@ class SignerProvider : ContentProvider() {
|
||||
val isRemembered = account.savedApps[key] ?: false
|
||||
if (!isRemembered) return null
|
||||
|
||||
account.history.add(History(packageName, "GET_PUBLIC_KEY", TimeUtils.now(), null))
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
|
||||
val cursor = MatrixCursor(arrayOf("signature"))
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.greenart7c3.nostrsigner.models
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.greenart7c3.nostrsigner.service.KeyPair
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
|
||||
class History(
|
||||
val appName: String,
|
||||
@@ -15,9 +17,9 @@ class History(
|
||||
@Stable
|
||||
class Account(
|
||||
val keyPair: KeyPair,
|
||||
val signer: NostrSigner = NostrSignerInternal(keyPair),
|
||||
var name: String,
|
||||
var savedApps: MutableMap<String, Boolean>,
|
||||
var history: MutableList<History> = mutableListOf()
|
||||
var savedApps: MutableMap<String, Boolean>
|
||||
) {
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,39 @@ package com.greenart7c3.nostrsigner.models
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
|
||||
data class Permission(
|
||||
val type: String,
|
||||
val kind: Int?,
|
||||
var checked: Boolean = true
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return when (type) {
|
||||
"nip04_encrypt" -> {
|
||||
"Encrypt data using nip 4"
|
||||
}
|
||||
"nip04_decrypt" -> {
|
||||
"Decrypt data using nip 4"
|
||||
}
|
||||
"nip44_decrypt" -> {
|
||||
"Decrypt data using nip 44"
|
||||
}
|
||||
"nip44_encrypt" -> {
|
||||
"Encrypt data using nip 44"
|
||||
}
|
||||
"decrypt_zap_event" -> {
|
||||
"Decrypt private zaps"
|
||||
}
|
||||
"sign_event" -> {
|
||||
when (kind) {
|
||||
22242 -> "Client authentication"
|
||||
else -> "Event kind $kind"
|
||||
}
|
||||
}
|
||||
else -> type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IntentData(
|
||||
val data: String,
|
||||
val name: String,
|
||||
@@ -10,7 +43,8 @@ class IntentData(
|
||||
val id: String,
|
||||
val callBackUrl: String?,
|
||||
val compression: CompressionType,
|
||||
val returnType: ReturnType
|
||||
val returnType: ReturnType,
|
||||
val permissions: List<Permission>?
|
||||
)
|
||||
|
||||
enum class SignerType {
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.greenart7c3.nostrsigner.service
|
||||
import android.util.Log
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.Nip44Version
|
||||
import com.vitorpamplona.quartz.crypto.decodeNIP44
|
||||
@@ -13,7 +12,6 @@ import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import java.nio.charset.Charset
|
||||
@@ -23,49 +21,6 @@ import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object AmberUtils {
|
||||
fun getSignedEvent(unsignedEvent: AmberEvent, privateKey: ByteArray): AmberEvent {
|
||||
val id = unsignedEvent.id.hexToByteArray()
|
||||
val sig = CryptoUtils.sign(id, privateKey).toHexKey()
|
||||
return AmberEvent(
|
||||
unsignedEvent.id,
|
||||
unsignedEvent.pubKey,
|
||||
unsignedEvent.createdAt,
|
||||
unsignedEvent.kind,
|
||||
unsignedEvent.tags,
|
||||
unsignedEvent.content,
|
||||
sig
|
||||
)
|
||||
}
|
||||
fun getZapRequestEvent(localEvent: LnZapRequestEvent, privateKey: ByteArray): LnZapRequestEvent {
|
||||
val originalNoteId = localEvent.zappedPost().firstOrNull()
|
||||
val pubkey = localEvent.zappedAuthor()[0]
|
||||
var privkey = privateKey
|
||||
val encryptionPrivateKey = LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
privkey.toHexKey(),
|
||||
originalNoteId ?: pubkey,
|
||||
localEvent.createdAt
|
||||
)
|
||||
val noteJson = (LnZapPrivateEvent.create(privkey, listOf(localEvent.tags[0], localEvent.tags[1]), localEvent.content)).toJson()
|
||||
val encryptedContent = encryptPrivateZapMessage(
|
||||
noteJson,
|
||||
encryptionPrivateKey,
|
||||
pubkey.hexToByteArray()
|
||||
)
|
||||
var tags = localEvent.tags.filter { !it.contains("anon") }
|
||||
tags = tags + listOf(listOf("anon", encryptedContent))
|
||||
privkey = encryptionPrivateKey // sign event with generated privkey
|
||||
val pubKey = CryptoUtils.pubkeyCreate(encryptionPrivateKey).toHexKey() // updated event with according pubkey
|
||||
|
||||
val id = AmberEvent.generateId(
|
||||
pubKey,
|
||||
localEvent.createdAt,
|
||||
LnZapRequestEvent.kind,
|
||||
tags,
|
||||
""
|
||||
)
|
||||
val sig = CryptoUtils.sign(id, privkey)
|
||||
return LnZapRequestEvent(id.toHexKey(), pubKey, localEvent.createdAt, tags, "", sig.toHexKey())
|
||||
}
|
||||
fun encryptOrDecryptData(data: String, type: SignerType, account: Account, pubKey: HexKey): String? {
|
||||
return when (type) {
|
||||
SignerType.DECRYPT_ZAP_EVENT -> {
|
||||
@@ -74,20 +29,20 @@ object AmberUtils {
|
||||
SignerType.NIP04_DECRYPT -> {
|
||||
CryptoUtils.decryptNIP04(
|
||||
data,
|
||||
account.keyPair.privKey,
|
||||
account.keyPair.privKey!!,
|
||||
Hex.decode(pubKey)
|
||||
)
|
||||
}
|
||||
SignerType.NIP04_ENCRYPT -> {
|
||||
CryptoUtils.encryptNIP04(
|
||||
data,
|
||||
account.keyPair.privKey,
|
||||
account.keyPair.privKey!!,
|
||||
Hex.decode(pubKey)
|
||||
)
|
||||
}
|
||||
SignerType.NIP44_ENCRYPT -> {
|
||||
val sharedSecret = CryptoUtils.getSharedSecretNIP44(
|
||||
account.keyPair.privKey,
|
||||
account.keyPair.privKey!!,
|
||||
pubKey.hexToByteArray()
|
||||
)
|
||||
|
||||
@@ -103,12 +58,12 @@ object AmberUtils {
|
||||
when (toDecrypt.v) {
|
||||
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(
|
||||
toDecrypt,
|
||||
account.keyPair.privKey,
|
||||
account.keyPair.privKey!!,
|
||||
pubKey.hexToByteArray()
|
||||
)
|
||||
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(
|
||||
toDecrypt,
|
||||
account.keyPair.privKey,
|
||||
account.keyPair.privKey!!,
|
||||
pubKey.hexToByteArray()
|
||||
)
|
||||
else -> null
|
||||
@@ -128,18 +83,18 @@ object AmberUtils {
|
||||
// if the receiver is logged in, these are the params.
|
||||
val pubkeyToUse = event.pubKey
|
||||
|
||||
event.getPrivateZapEvent(loggedInPrivateKey, pubkeyToUse)?.toJson() ?: ""
|
||||
event.getPrivateZapEvent(loggedInPrivateKey!!, pubkeyToUse)?.toJson() ?: ""
|
||||
} else {
|
||||
// if the sender is logged in, these are the params
|
||||
val altPrivateKeyToUse = if (recipientPost != null) {
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
loggedInPrivateKey.toHexKey(),
|
||||
loggedInPrivateKey!!.toHexKey(),
|
||||
recipientPost,
|
||||
event.createdAt
|
||||
)
|
||||
} else if (recipientPK != null) {
|
||||
LnZapRequestEvent.createEncryptionPrivateKey(
|
||||
loggedInPrivateKey.toHexKey(),
|
||||
loggedInPrivateKey!!.toHexKey(),
|
||||
recipientPK,
|
||||
event.createdAt
|
||||
)
|
||||
@@ -153,7 +108,6 @@ object AmberUtils {
|
||||
|
||||
if (altPubKeyFromPrivate == event.pubKey) {
|
||||
val result = event.getPrivateZapEvent(altPrivateKeyToUse, recipientPK)
|
||||
|
||||
result?.toJson() ?: ""
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -2,11 +2,15 @@ package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.greenart7c3.nostrsigner.models.CompressionType
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.models.Permission
|
||||
import com.greenart7c3.nostrsigner.models.ReturnType
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import java.net.URLDecoder
|
||||
|
||||
object IntentUtils {
|
||||
@@ -41,6 +45,11 @@ object IntentUtils {
|
||||
|
||||
val compressionType = if (intent.extras?.getString("compression") == "gzip") CompressionType.GZIP else CompressionType.NONE
|
||||
val returnType = if (intent.extras?.getString("returnType") == "event") ReturnType.EVENT else ReturnType.SIGNATURE
|
||||
val listType = object : TypeToken<List<Permission>>() {}.type
|
||||
val permissions = Gson().fromJson<List<Permission>?>(intent.extras?.getString("permissions"), listType)
|
||||
permissions?.forEach {
|
||||
it.checked = true
|
||||
}
|
||||
|
||||
return IntentData(
|
||||
data,
|
||||
@@ -50,7 +59,8 @@ object IntentUtils {
|
||||
id,
|
||||
intent.extras?.getString("callbackUrl"),
|
||||
compressionType,
|
||||
returnType
|
||||
returnType,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
class KeyPair(
|
||||
privKey: ByteArray? = null,
|
||||
pubKey: ByteArray? = null
|
||||
) {
|
||||
val privKey: ByteArray
|
||||
val pubKey: ByteArray
|
||||
|
||||
init {
|
||||
if (privKey == null) {
|
||||
this.privKey = CryptoUtils.privkeyCreate()
|
||||
this.pubKey = CryptoUtils.pubkeyCreate(this.privKey)
|
||||
} else {
|
||||
// as private key is provided, ignore the public key and set keys according to private key
|
||||
this.privKey = privKey
|
||||
this.pubKey = pubKey ?: CryptoUtils.pubkeyCreate(privKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "KeyPair(privateKey=${privKey.toHexKey()}, publicKey=${pubKey.toHexKey()}"
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,13 @@ import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.KeyPair
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import java.lang.reflect.Type
|
||||
import java.math.BigDecimal
|
||||
@@ -64,6 +65,8 @@ open class AmberEvent(
|
||||
|
||||
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
|
||||
|
||||
fun relay() = tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first()
|
||||
|
||||
override fun isSensitive() = tags.any {
|
||||
(it.size > 0 && it[0].equals("content-warning", true)) ||
|
||||
(it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) ||
|
||||
@@ -419,6 +422,18 @@ open class AmberEvent(
|
||||
else -> this
|
||||
}
|
||||
|
||||
fun toEvent(amberEvent: AmberEvent): Event {
|
||||
return Event(
|
||||
amberEvent.id,
|
||||
amberEvent.pubKey,
|
||||
amberEvent.createdAt,
|
||||
amberEvent.kind,
|
||||
amberEvent.tags,
|
||||
amberEvent.content,
|
||||
amberEvent.sig
|
||||
)
|
||||
}
|
||||
|
||||
fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List<List<String>>, content: String): ByteArray {
|
||||
val rawEvent = listOf(
|
||||
0,
|
||||
@@ -444,7 +459,7 @@ open class AmberEvent(
|
||||
if (event.pubKey.isEmpty()) {
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val id = generateId(pubKey, event.createdAt, event.kind, event.tags, event.content)
|
||||
val signature = CryptoUtils.sign(id, keyPair.privKey).toHexKey()
|
||||
val signature = CryptoUtils.sign(id, keyPair.privKey!!).toHexKey()
|
||||
return AmberEvent(id.toHexKey(), pubKey, event.createdAt, event.kind, event.tags, event.content, signature)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ import com.greenart7c3.nostrsigner.service.IntentUtils
|
||||
fun AccountScreen(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
intent: Intent,
|
||||
packageName: String?
|
||||
packageName: String?,
|
||||
appName: String?
|
||||
) {
|
||||
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||
|
||||
@@ -30,7 +31,7 @@ fun AccountScreen(
|
||||
is AccountState.LoggedIn -> {
|
||||
val intentData = IntentUtils.getIntentData(intent)
|
||||
|
||||
MainScreen(state.account, accountStateViewModel, intentData, packageName, state.route)
|
||||
MainScreen(state.account, accountStateViewModel, intentData, packageName, appName, state.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.service.KeyPair
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -62,9 +62,9 @@ class AccountStateViewModel(npub: String?) : ViewModel() {
|
||||
|
||||
fun startUI(key: String, route: String?) {
|
||||
val account = if (key.startsWith("nsec")) {
|
||||
Account(KeyPair(privKey = key.bechToBytes()), "", mutableMapOf())
|
||||
Account(KeyPair(privKey = key.bechToBytes()), name = "", savedApps = mutableMapOf())
|
||||
} else {
|
||||
Account(KeyPair(Hex.decode(key)), "", mutableMapOf())
|
||||
Account(KeyPair(Hex.decode(key)), name = "", savedApps = mutableMapOf())
|
||||
}
|
||||
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
@@ -72,7 +72,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() {
|
||||
}
|
||||
|
||||
fun newKey() {
|
||||
val account = Account(KeyPair(), "", mutableMapOf())
|
||||
val account = Account(KeyPair(), name = "", savedApps = mutableMapOf())
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
startUI(account, null)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ fun HomeScreen(
|
||||
modifier: Modifier,
|
||||
json: IntentData?,
|
||||
packageName: String?,
|
||||
applicationName: String?,
|
||||
account: Account
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -63,13 +64,11 @@ fun HomeScreen(
|
||||
val remember = remember {
|
||||
mutableStateOf(account.savedApps[key] ?: false)
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
LoginWithPubKey(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
{
|
||||
applicationName,
|
||||
json.permissions,
|
||||
{ permissions ->
|
||||
val sig = account.keyPair.pubKey.toNpub()
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
@@ -81,15 +80,15 @@ fun HomeScreen(
|
||||
clipboardManager,
|
||||
sig,
|
||||
sig,
|
||||
it
|
||||
it,
|
||||
permissions = permissions
|
||||
)
|
||||
}
|
||||
return@LoginWithPubKey
|
||||
},
|
||||
{
|
||||
context.getAppCompatActivity()?.finish()
|
||||
},
|
||||
account
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,9 +98,11 @@ fun HomeScreen(
|
||||
}
|
||||
val shouldRunOnAccept = account.savedApps[key] ?: false
|
||||
EncryptDecryptData(
|
||||
it.data,
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
applicationName,
|
||||
appName,
|
||||
it.type,
|
||||
{
|
||||
@@ -210,8 +211,9 @@ fun HomeScreen(
|
||||
remember,
|
||||
packageName,
|
||||
appName,
|
||||
applicationName,
|
||||
event,
|
||||
event.toJson(),
|
||||
it.data,
|
||||
{
|
||||
if (event.pubKey != account.keyPair.pubKey.toHexKey()) {
|
||||
coroutineScope.launch {
|
||||
@@ -229,31 +231,8 @@ fun HomeScreen(
|
||||
} catch (e: Exception) {
|
||||
Event.fromJson(event.toJson())
|
||||
}
|
||||
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(),
|
||||
resultEvent.toJson(),
|
||||
it,
|
||||
resultEvent.kind
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val signedEvent = AmberUtils.getSignedEvent(
|
||||
event,
|
||||
account.keyPair.privKey
|
||||
)
|
||||
|
||||
account.signer.sign<Event>(localEvent.createdAt, localEvent.kind, localEvent.tags, localEvent.content) { signedEvent ->
|
||||
coroutineScope.launch {
|
||||
sendResult(
|
||||
context,
|
||||
@@ -263,9 +242,8 @@ fun HomeScreen(
|
||||
remember.value,
|
||||
clipboardManager,
|
||||
signedEvent.toJson(),
|
||||
signedEvent.sig,
|
||||
it,
|
||||
signedEvent.kind
|
||||
if (localEvent is LnZapRequestEvent && localEvent.tags.any { tag -> tag.any { t -> t == "anon" } }) signedEvent.toJson() else signedEvent.sig,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.ui.theme.Font14SP
|
||||
@@ -76,7 +76,7 @@ fun LoginPage(
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// The first child is glued to the top.
|
||||
// Hence we have nothing at the top, an empty box is used.
|
||||
@@ -205,23 +205,20 @@ fun LoginPage(
|
||||
Text(text = stringResource(R.string.login))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The last child is glued to the bottom.
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
accountViewModel.newKey()
|
||||
},
|
||||
style = TextStyle(
|
||||
fontSize = Font14SP,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
||||
modifier = Modifier
|
||||
.padding(30.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
accountViewModel.newKey()
|
||||
},
|
||||
style = TextStyle(
|
||||
fontSize = Font14SP,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,19 @@ import android.widget.Toast
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
@@ -46,7 +45,7 @@ import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -59,14 +58,15 @@ import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.CompressionType
|
||||
import com.greenart7c3.nostrsigner.models.History
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.models.Permission
|
||||
import com.greenart7c3.nostrsigner.models.ReturnType
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import com.greenart7c3.nostrsigner.service.getAppCompatActivity
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
|
||||
import com.greenart7c3.nostrsigner.ui.navigation.Route
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
@@ -83,21 +83,28 @@ fun sendResult(
|
||||
event: String,
|
||||
value: String,
|
||||
intentData: IntentData,
|
||||
kind: Int? = null
|
||||
permissions: List<Permission>? = null
|
||||
) {
|
||||
val activity = context.getAppCompatActivity()
|
||||
if (packageName != null) {
|
||||
permissions?.filter { it.checked }?.forEach {
|
||||
val type = it.type.toUpperCase(Locale.current)
|
||||
val permissionKey = if (it.type == "sign_event") "$packageName-$type-${it.kind}" else "$packageName-$type"
|
||||
account.savedApps[permissionKey] = true
|
||||
}
|
||||
|
||||
if (rememberChoice) {
|
||||
account.savedApps[key] = true
|
||||
}
|
||||
|
||||
account.history.add(History(packageName, intentData.type.toString(), TimeUtils.now(), kind))
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
val intent = Intent()
|
||||
intent.putExtra("signature", value)
|
||||
intent.putExtra("id", intentData.id)
|
||||
intent.putExtra("event", event)
|
||||
intent.putExtra("package", BuildConfig.APPLICATION_ID)
|
||||
if (intentData.type == SignerType.GET_PUBLIC_KEY) {
|
||||
intent.putExtra("package", BuildConfig.APPLICATION_ID)
|
||||
}
|
||||
activity?.setResult(RESULT_OK, intent)
|
||||
} else if (intentData.callBackUrl != null) {
|
||||
if (intentData.returnType == ReturnType.SIGNATURE) {
|
||||
@@ -167,11 +174,12 @@ fun MainScreen(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
json: IntentData?,
|
||||
packageName: String?,
|
||||
appName: String?,
|
||||
route: String?
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.History, Route.Settings)
|
||||
val items = listOf(Route.Home, Route.Permissions, Route.Settings)
|
||||
var shouldShowBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
@@ -180,6 +188,7 @@ fun MainScreen(
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
@@ -210,12 +219,21 @@ fun MainScreen(
|
||||
title = {
|
||||
val name = LocalPreferences.getAccountName(account.keyPair.pubKey.toNpub())
|
||||
Row(
|
||||
Modifier.clickable {
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
shouldShowBottomSheet = true
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
.border(
|
||||
border = ButtonDefaults.outlinedButtonBorder,
|
||||
shape = ButtonBorder
|
||||
)
|
||||
.padding(8.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null
|
||||
) {
|
||||
scope.launch {
|
||||
sheetState.show()
|
||||
shouldShowBottomSheet = true
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(name.ifBlank { account.keyPair.pubKey.toNpub().toShortenHex() })
|
||||
@@ -272,6 +290,7 @@ fun MainScreen(
|
||||
.padding(padding),
|
||||
json,
|
||||
packageName,
|
||||
appName,
|
||||
account
|
||||
)
|
||||
}
|
||||
@@ -304,41 +323,6 @@ fun MainScreen(
|
||||
}
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.History.route,
|
||||
content = {
|
||||
val history = LocalPreferences.loadHistory(account.keyPair.pubKey.toNpub()).sortedWith(compareByDescending { it.time })
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp),
|
||||
state = listState
|
||||
) {
|
||||
items(history.size) {
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(history[it].appName)
|
||||
Text(history[it].type.toLowerCase(Locale.current))
|
||||
history[it].kind?.let {
|
||||
Text("$it")
|
||||
}
|
||||
Text(timeAgoShort(history[it].time, "now"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
composable(
|
||||
Route.Permission.route,
|
||||
arguments = listOf(navArgument("packageName") { type = NavType.StringType }),
|
||||
|
||||
@@ -178,7 +178,7 @@ private fun copyNSec(
|
||||
account: Account,
|
||||
clipboardManager: ClipboardManager
|
||||
) {
|
||||
account.keyPair.privKey.let {
|
||||
account.keyPair.privKey?.let {
|
||||
clipboardManager.setText(AnnotatedString(it.toNsec()))
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
|
||||
@@ -7,9 +7,11 @@ 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.ButtonDefaults
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
@@ -28,16 +30,24 @@ fun AcceptRejectButtons(
|
||||
) {
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onReject
|
||||
onClick = onReject,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF5A5554)
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.reject))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = onAccept
|
||||
Row(
|
||||
Modifier.weight(0.8f)
|
||||
) {
|
||||
Text(stringResource(R.string.accept))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = ButtonBorder,
|
||||
onClick = onAccept
|
||||
) {
|
||||
Text(stringResource(R.string.accept))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -12,19 +13,31 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.SignerType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun EncryptDecryptData(
|
||||
content: String,
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
applicationName: String?,
|
||||
appName: String,
|
||||
type: SignerType,
|
||||
onAccept: () -> Unit,
|
||||
@@ -34,31 +47,51 @@ fun EncryptDecryptData(
|
||||
var showMore by androidx.compose.runtime.remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
val message = when (type) {
|
||||
SignerType.NIP44_ENCRYPT -> stringResource(R.string.encrypt_nip44)
|
||||
SignerType.NIP04_ENCRYPT -> stringResource(R.string.encrypt_nip04)
|
||||
SignerType.NIP44_DECRYPT -> stringResource(R.string.decrypt_nip44)
|
||||
SignerType.NIP04_DECRYPT -> stringResource(R.string.decrypt_nip04)
|
||||
SignerType.DECRYPT_ZAP_EVENT -> stringResource(R.string.decrypt_zap_event)
|
||||
else -> stringResource(R.string.encrypt_decrypt)
|
||||
}
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(applicationName ?: appName)
|
||||
}
|
||||
append(" requests $message")
|
||||
},
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val message = when (type) {
|
||||
SignerType.NIP44_ENCRYPT -> stringResource(R.string.encrypt_nip44)
|
||||
SignerType.NIP04_ENCRYPT -> stringResource(R.string.encrypt_nip04)
|
||||
SignerType.NIP44_DECRYPT -> stringResource(R.string.decrypt_nip44)
|
||||
SignerType.NIP04_DECRYPT -> stringResource(R.string.decrypt_nip04)
|
||||
SignerType.DECRYPT_ZAP_EVENT -> stringResource(R.string.decrypt_zap_event)
|
||||
else -> stringResource(R.string.encrypt_decrypt)
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.wants_you_to_data, message),
|
||||
"Event content",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
content,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
RawJsonButton(
|
||||
@@ -68,7 +101,21 @@ fun EncryptDecryptData(
|
||||
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details)
|
||||
)
|
||||
if (showMore) {
|
||||
RawJson(onCopy(), Modifier.weight(1f), stringResource(R.string.encrypted_decrypted_data))
|
||||
RawJson(
|
||||
content,
|
||||
Modifier.weight(1f),
|
||||
stringResource(R.string.encrypted_decrypted_data)
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(onCopy()))
|
||||
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.data_copied_to_the_clipboard),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
@@ -6,14 +6,7 @@ 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.material.icons.filled.Wifi
|
||||
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
|
||||
@@ -22,13 +15,15 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.TimeUtils
|
||||
import com.greenart7c3.nostrsigner.service.model.AmberEvent
|
||||
import com.greenart7c3.nostrsigner.service.toShortenHex
|
||||
|
||||
@Composable
|
||||
fun EventData(
|
||||
@@ -36,6 +31,7 @@ fun EventData(
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
applicationName: String?,
|
||||
event: AmberEvent,
|
||||
rawJson: String,
|
||||
onAccept: () -> Unit,
|
||||
@@ -44,65 +40,40 @@ fun EventData(
|
||||
var showMore by androidx.compose.runtime.remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val eventDescription = stringResource(R.string.event)
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
val text = if (event.kind == 22242) "requests client authentication" else "requests event signature"
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(applicationName ?: appName)
|
||||
}
|
||||
append(" $text")
|
||||
},
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(Modifier.padding(6.dp)) {
|
||||
val message = if (event.kind == 22242) {
|
||||
stringResource(R.string.client_authentication)
|
||||
} else {
|
||||
eventDescription
|
||||
}
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.wants_you_to_sign_a, message),
|
||||
"Event content",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.CalendarMonth,
|
||||
stringResource(R.string.kind),
|
||||
"${event.kind}"
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.AccessTime,
|
||||
stringResource(R.string.created_at),
|
||||
TimeUtils.format(event.createdAt)
|
||||
)
|
||||
|
||||
EventRow(
|
||||
Icons.Default.Person,
|
||||
stringResource(R.string.signed_by),
|
||||
event.pubKey.toShortenHex()
|
||||
)
|
||||
|
||||
if (event.kind == 22242) {
|
||||
EventRow(
|
||||
Icons.Default.Wifi,
|
||||
stringResource(R.string.relay),
|
||||
event.tags.firstOrNull { it.size > 1 && it[0] == "relay" }?.get(1)?.removePrefix("wss://")?.removePrefix("ws://") ?: ""
|
||||
)
|
||||
} else {
|
||||
EventRow(
|
||||
Icons.Default.ContentPaste,
|
||||
stringResource(R.string.content),
|
||||
event.content
|
||||
)
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 15.dp),
|
||||
thickness = Dp.Hairline
|
||||
val content = if (event.kind == 22242) event.relay() else event.content
|
||||
Text(
|
||||
content,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,252 @@
|
||||
package com.greenart7c3.nostrsigner.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
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 com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.greenart7c3.nostrsigner.models.Permission
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
|
||||
@Composable
|
||||
fun LoginWithPubKey(
|
||||
shouldRunOnAccept: Boolean,
|
||||
remember: MutableState<Boolean>,
|
||||
packageName: String?,
|
||||
appName: String,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit,
|
||||
account: Account
|
||||
applicationName: String?,
|
||||
permissions: List<Permission>?,
|
||||
onAccept: (List<Permission>?) -> Unit,
|
||||
onReject: () -> Unit
|
||||
) {
|
||||
var showMore by androidx.compose.runtime.remember {
|
||||
var localPermissions by remember {
|
||||
mutableStateOf(permissions)
|
||||
}
|
||||
|
||||
var showAdjustDialog by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (showAdjustDialog) {
|
||||
var dialogPermissions by remember {
|
||||
mutableStateOf(localPermissions?.map { it.copy() } ?: listOf())
|
||||
}
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
showAdjustDialog = false
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(
|
||||
onCancel = {
|
||||
showAdjustDialog = false
|
||||
}
|
||||
)
|
||||
|
||||
PostButton(
|
||||
isActive = true,
|
||||
onPost = {
|
||||
localPermissions = dialogPermissions.map {
|
||||
it.copy()
|
||||
}
|
||||
showAdjustDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
Modifier.padding(16.dp)
|
||||
) {
|
||||
items(dialogPermissions.size) {
|
||||
val permission = dialogPermissions[it]
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
dialogPermissions = dialogPermissions.mapIndexed { j, item ->
|
||||
if (it == j) {
|
||||
item.copy(checked = !item.checked)
|
||||
} else {
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = permission.toString()
|
||||
)
|
||||
Switch(
|
||||
checked = permission.checked,
|
||||
onCheckedChange = { _ ->
|
||||
dialogPermissions = dialogPermissions.mapIndexed { j, item ->
|
||||
if (it == j) {
|
||||
item.copy(checked = !item.checked)
|
||||
} else {
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
AppTitle(appName)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(applicationName ?: appName)
|
||||
}
|
||||
append(" would like your permission to read your public key and sign events on your behalf")
|
||||
},
|
||||
fontSize = 18.sp
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
|
||||
localPermissions?.let {
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(6.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.wants_to_read_your_public_key),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
itemsIndexed(it.take(3)) { _, item ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
if (item.checked) Icons.Default.Done else Icons.Default.Close,
|
||||
item.toString(),
|
||||
tint = if (item.checked) Color.Green else Color.Red
|
||||
)
|
||||
Text(text = item.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (it.size > 3) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
val isAnyChecked = it.take(3).any { permission -> permission.checked }
|
||||
Icon(
|
||||
if (isAnyChecked) Icons.Default.Done else Icons.Default.Close,
|
||||
"...and more",
|
||||
tint = if (isAnyChecked) Color.Green else Color.Red
|
||||
)
|
||||
Text(text = "${it.size - 3} more...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RawJsonButton(
|
||||
onCLick = {
|
||||
showMore = !showMore
|
||||
},
|
||||
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (showMore) {
|
||||
RawJson(account.keyPair.pubKey.toNpub(), Modifier.weight(1f), stringResource(R.string.my_public_key))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
Arrangement.Center,
|
||||
Alignment.CenterHorizontally
|
||||
) {
|
||||
localPermissions?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showAdjustDialog = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(text = "Adjust")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.8f)
|
||||
.padding(8.dp),
|
||||
shape = ButtonBorder,
|
||||
onClick = {
|
||||
onAccept(localPermissions)
|
||||
}
|
||||
) {
|
||||
Text("Grant Permissions")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
shape = ButtonBorder,
|
||||
onClick = onReject,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF5A5554)
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.reject))
|
||||
}
|
||||
}
|
||||
|
||||
RememberMyChoice(
|
||||
shouldRunOnAccept,
|
||||
remember,
|
||||
packageName,
|
||||
onAccept
|
||||
)
|
||||
|
||||
AcceptRejectButtons(
|
||||
onAccept,
|
||||
onReject
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ fun PostButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive:
|
||||
shape = ButtonBorder,
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text(text = "Post", color = Color.White)
|
||||
Text(text = "Save", color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,27 +18,22 @@ 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,
|
||||
label: String = stringResource(R.string.copy_raw_json)
|
||||
label: String = stringResource(R.string.copy_raw_json),
|
||||
onCopy: (() -> Unit)? = null
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val value = try {
|
||||
JSONObject(rawJson).toString(2)
|
||||
} catch (e: Exception) {
|
||||
rawJson
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
value = TextFieldValue(value),
|
||||
value = TextFieldValue(rawJson),
|
||||
onValueChange = { },
|
||||
readOnly = true
|
||||
)
|
||||
@@ -49,14 +44,18 @@ fun RawJson(
|
||||
Button(
|
||||
shape = ButtonBorder,
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(rawJson))
|
||||
if (onCopy != null) {
|
||||
onCopy()
|
||||
} else {
|
||||
clipboardManager.setText(AnnotatedString(rawJson))
|
||||
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.data_copied_to_the_clipboard),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.data_copied_to_the_clipboard),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
@@ -3,11 +3,13 @@ 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.ButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder
|
||||
|
||||
@@ -22,9 +24,12 @@ fun RawJsonButton(
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Button(
|
||||
TextButton(
|
||||
shape = ButtonBorder,
|
||||
onClick = onCLick
|
||||
onClick = onCLick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = Color.Black
|
||||
)
|
||||
) {
|
||||
Text(text)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ fun EventDataPreview() {
|
||||
remember,
|
||||
null,
|
||||
"App",
|
||||
null,
|
||||
event,
|
||||
data,
|
||||
{ },
|
||||
|
||||
@@ -28,29 +28,29 @@ val Font14SP = 14.sp
|
||||
|
||||
val Size35dp = 35.dp
|
||||
|
||||
val Purple200 = Color(0xFFBB86FC)
|
||||
val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
|
||||
val ButtonBorder = RoundedCornerShape(20.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
|
||||
val primaryColor = Color(0xFFF16F26)
|
||||
val primaryVariant = Color(0xFFC8541A)
|
||||
val secondaryColor = Color(0xFFFF8E4C)
|
||||
|
||||
private val DarkColorPalette = darkColorScheme(
|
||||
primary = Purple200,
|
||||
secondary = Purple700,
|
||||
tertiary = Teal200
|
||||
primary = primaryColor,
|
||||
secondary = primaryVariant,
|
||||
tertiary = secondaryColor,
|
||||
primaryContainer = secondaryColor,
|
||||
secondaryContainer = secondaryColor
|
||||
)
|
||||
|
||||
private val LightColorPalette = lightColorScheme(
|
||||
primary = Purple500,
|
||||
secondary = Purple700,
|
||||
tertiary = Teal200
|
||||
primary = primaryColor,
|
||||
secondary = primaryVariant,
|
||||
tertiary = secondaryColor,
|
||||
primaryContainer = secondaryColor,
|
||||
secondaryContainer = secondaryColor
|
||||
)
|
||||
|
||||
private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
|
||||
@Composable
|
||||
fun NostrSignerTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
|
||||
</vector>
|
||||
@@ -1,4 +0,0 @@
|
||||
<vector android:height="120dp" android:viewportHeight="47"
|
||||
android:viewportWidth="47" android:width="120dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#712F8F" android:fillType="evenOdd" android:pathData="M35,11C37.209,11 39,9.209 39,7C39,4.791 37.209,3 35,3C32.791,3 31,4.791 31,7C31,9.209 32.791,11 35,11ZM35,14C38.866,14 42,10.866 42,7C42,3.134 38.866,0 35,0C31.134,0 28,3.134 28,7C28,8.359 28.387,9.627 29.057,10.7L26.611,13.146C25.019,11.828 22.976,11.035 20.747,11.035C18.586,11.035 16.598,11.781 15.029,13.029L11.259,9.259C11.73,8.448 12,7.505 12,6.5C12,3.462 9.538,1 6.5,1C3.462,1 1,3.462 1,6.5C1,9.538 3.462,12 6.5,12C7.442,12 8.328,11.763 9.103,11.346L13.012,15.255C12.088,16.689 11.552,18.397 11.552,20.23C11.552,22.265 12.213,24.145 13.332,25.669L11.709,27.291C10.509,26.476 9.06,26 7.5,26C3.358,26 0,29.358 0,33.5C0,37.642 3.358,41 7.5,41C11.642,41 15,37.642 15,33.5C15,32.003 14.561,30.608 13.805,29.437L15.477,27.766C16.969,28.812 18.786,29.426 20.747,29.426C22.612,29.426 24.347,28.871 25.796,27.917L30.589,32.711C29.588,34.132 29,35.865 29,37.736C29,42.56 32.911,46.471 37.736,46.471C42.56,46.471 46.471,42.56 46.471,37.736C46.471,32.911 42.56,29 37.736,29C35.865,29 34.132,29.588 32.71,30.589L28.002,25.881C29.218,24.322 29.942,22.361 29.942,20.23C29.942,18.465 29.445,16.817 28.583,15.417L31.152,12.848C32.256,13.576 33.578,14 35,14ZM9,6.5C9,7.881 7.881,9 6.5,9C5.119,9 4,7.881 4,6.5C4,5.119 5.119,4 6.5,4C7.881,4 9,5.119 9,6.5ZM7.5,38C9.985,38 12,35.985 12,33.5C12,31.015 9.985,29 7.5,29C5.015,29 3,31.015 3,33.5C3,35.985 5.015,38 7.5,38ZM43.471,37.736C43.471,40.903 40.903,43.471 37.736,43.471C34.568,43.471 32,40.903 32,37.736C32,34.568 34.568,32 37.736,32C40.903,32 43.471,34.568 43.471,37.736ZM26.942,20.23C26.942,23.652 24.169,26.426 20.747,26.426C17.326,26.426 14.552,23.652 14.552,20.23C14.552,16.809 17.326,14.035 20.747,14.035C24.169,14.035 26.942,16.809 26.942,20.23Z"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user