WIP
This commit is contained in:
@@ -55,6 +55,7 @@ dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.biometric)
|
||||
implementation(libs.play.services.code.scanner)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -10,6 +12,9 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GajuMobile">
|
||||
|
||||
<meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="barcode_ui"/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -41,11 +41,13 @@ import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.MnemonicVerifyScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.QRReceiveScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.QRScannerScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.SendFormScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.SettingsScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.SetupChoiceScreen
|
||||
import swiss.qpq.gajumobile.ui.screens.ViewMnemonicScreen
|
||||
import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme
|
||||
import swiss.qpq.gajumaru.core.formatting.GajuFormat
|
||||
import swiss.qpq.gajumaru.core.tools.Grids
|
||||
import java.util.UUID
|
||||
|
||||
class MainActivity : FragmentActivity() {
|
||||
@@ -75,10 +77,9 @@ class MainActivity : FragmentActivity() {
|
||||
lockErrorMessage = null
|
||||
SecurityManager.unlock()
|
||||
},
|
||||
onError = { err ->
|
||||
lockErrorMessage = err
|
||||
}
|
||||
)
|
||||
) { err ->
|
||||
lockErrorMessage = err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val routerError: (String) -> Unit = { message ->
|
||||
@@ -106,14 +107,15 @@ enum class Screen {
|
||||
EXPLORER,
|
||||
VIEW_MNEMONIC,
|
||||
VERIFY_MNEMONIC,
|
||||
ENVIRONMENT_INFO
|
||||
ENVIRONMENT_INFO,
|
||||
SEND_FORM
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GajuRouter(
|
||||
walletRepo: WalletRepository,
|
||||
appStateManager: AppStateManager,
|
||||
onError: (String) -> Unit
|
||||
onError: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val appState by appStateManager.state.collectAsState()
|
||||
@@ -140,8 +142,9 @@ fun GajuRouter(
|
||||
}
|
||||
|
||||
var pendingWalletName by rememberSaveable { mutableStateOf("") }
|
||||
var pendingNetworkId by rememberSaveable { mutableStateOf("groot.mainnet") }
|
||||
var pendingAccount by remember { mutableStateOf<Account?>(null) }
|
||||
var setupRecovered by rememberSaveable { mutableStateOf(false) }
|
||||
var setupRecovered by rememberSaveable { mutableStateOf(value = false) }
|
||||
|
||||
var selectedWalletId by rememberSaveable { mutableStateOf("") }
|
||||
var selectedAccountId by rememberSaveable { mutableStateOf("") }
|
||||
@@ -158,15 +161,18 @@ fun GajuRouter(
|
||||
var activeWalletIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||
var activeAccountIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||
|
||||
var pendingScanResult by remember { mutableStateOf<Grids.ParseResult?>(null) }
|
||||
|
||||
when (currentScreen) {
|
||||
Screen.CREATE_WALLET -> {
|
||||
CreateWalletScreen(
|
||||
onWalletCreated = { name ->
|
||||
onWalletCreated = { name, networkId ->
|
||||
pendingWalletName = name
|
||||
pendingNetworkId = networkId
|
||||
selectedWalletId = ""
|
||||
navigateTo(Screen.SETUP_CHOICE)
|
||||
},
|
||||
onBack = if (screenStack.size > 1) { { navigateBack() } } else null
|
||||
onBack = if (screenStack.size > 1) { { navigateBack() } } else null,
|
||||
)
|
||||
}
|
||||
Screen.SETUP_CHOICE -> {
|
||||
@@ -189,7 +195,7 @@ fun GajuRouter(
|
||||
setupRecovered = true
|
||||
navigateTo(Screen.MNEMONIC_RECOVERY)
|
||||
},
|
||||
onBack = { navigateBack() }
|
||||
onBack = { navigateBack() },
|
||||
)
|
||||
}
|
||||
Screen.MNEMONIC_RECOVERY -> {
|
||||
@@ -206,7 +212,7 @@ fun GajuRouter(
|
||||
onError("Failed to recover account: ${e.message}")
|
||||
}
|
||||
},
|
||||
onBack = { navigateBack() }
|
||||
onBack = { navigateBack() },
|
||||
)
|
||||
}
|
||||
Screen.ACCOUNT_NAMING -> {
|
||||
@@ -220,14 +226,17 @@ fun GajuRouter(
|
||||
|
||||
if (selectedWalletId.isNotEmpty()) {
|
||||
val wallet = walletRepo.loadWallet(selectedWalletId)
|
||||
if (wallet != null) {
|
||||
walletRepo.saveWallet(wallet.copy(accounts = wallet.accounts + finalAccount))
|
||||
wallet?.let {
|
||||
walletRepo.saveWallet(it.copy(accounts = it.accounts + finalAccount))
|
||||
}
|
||||
} else {
|
||||
val endpoints = if (pendingNetworkId == "groot.mainnet") Wallet.DEFAULT_MAINNET else Wallet.DEFAULT_TESTNET
|
||||
val wallet = Wallet(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = pendingWalletName.ifBlank { "My Wallet" },
|
||||
accounts = listOf(finalAccount)
|
||||
networkId = pendingNetworkId,
|
||||
endpoints = endpoints,
|
||||
accounts = listOf(finalAccount),
|
||||
)
|
||||
walletRepo.saveWallet(wallet)
|
||||
}
|
||||
@@ -241,7 +250,7 @@ fun GajuRouter(
|
||||
screenStack.clear()
|
||||
screenStack.add(Screen.DASHBOARD)
|
||||
},
|
||||
onBack = { navigateBack() }
|
||||
onBack = { navigateBack() },
|
||||
)
|
||||
}
|
||||
Screen.VIEW_MNEMONIC -> {
|
||||
@@ -323,7 +332,7 @@ fun GajuRouter(
|
||||
"manage_wallets" -> Screen.MANAGE_WALLETS
|
||||
else -> null
|
||||
}
|
||||
if (screen != null) navigateTo(screen)
|
||||
screen?.let { navigateTo(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -353,14 +362,14 @@ fun GajuRouter(
|
||||
navigateTo(Screen.DELETE_CONFIRM)
|
||||
}
|
||||
},
|
||||
onUpdateNetwork = { id, network ->
|
||||
onUpdateEndpoints = { id, endpoints ->
|
||||
val wallet = walletRepo.loadWallet(id)
|
||||
wallet?.let {
|
||||
walletRepo.saveWallet(it.copy(network = network))
|
||||
walletRepo.saveWallet(it.copy(endpoints = endpoints))
|
||||
appStateManager.notifyWalletsChanged()
|
||||
}
|
||||
},
|
||||
onBack = { navigateBack() }
|
||||
onBack = { navigateBack() },
|
||||
)
|
||||
}
|
||||
Screen.MANAGE_ACCOUNT -> {
|
||||
@@ -436,19 +445,96 @@ fun GajuRouter(
|
||||
Screen.ENVIRONMENT_INFO -> {
|
||||
EnvironmentInfoScreen(onBack = { navigateBack() })
|
||||
}
|
||||
Screen.QR_SCANNER -> QRScannerScreen(onGenerate = { navigateTo(Screen.QR_RECEIVE) }, onUpload = {})
|
||||
Screen.QR_RECEIVE -> QRReceiveScreen(onDone = { navigateBack() })
|
||||
Screen.EXPLORER -> ExplorerScreen(onNavigate = { dest ->
|
||||
val screen = when (dest) {
|
||||
"dashboard" -> Screen.DASHBOARD
|
||||
"settings" -> Screen.SETTINGS
|
||||
"manage_wallets" -> Screen.MANAGE_WALLETS
|
||||
else -> null
|
||||
Screen.QR_SCANNER -> QRScannerScreen(
|
||||
onScanResult = { url ->
|
||||
try {
|
||||
val result = Grids.parse(url)
|
||||
pendingScanResult = result
|
||||
navigateTo(Screen.SEND_FORM)
|
||||
} catch (e: Exception) {
|
||||
onError("Invalid GRIDS URL: ${e.message}")
|
||||
}
|
||||
},
|
||||
onGenerate = { navigateTo(Screen.QR_RECEIVE) },
|
||||
onUpload = {},
|
||||
onManualEntry = {
|
||||
pendingScanResult = null
|
||||
navigateTo(Screen.SEND_FORM)
|
||||
},
|
||||
onBack = { navigateBack() },
|
||||
)
|
||||
Screen.SEND_FORM -> {
|
||||
val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) }
|
||||
val activeWallet = wallets.getOrNull(activeWalletIndex)
|
||||
val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex)
|
||||
|
||||
if (activeAccount == null) {
|
||||
onError("No active account selected.")
|
||||
navigateBack()
|
||||
return
|
||||
}
|
||||
if (screen != null) {
|
||||
screenStack.clear()
|
||||
screenStack.add(screen)
|
||||
|
||||
val result = pendingScanResult
|
||||
val initialRecipient = result?.recipient ?: ""
|
||||
val initialAmount = if (result?.amount != null) {
|
||||
val type = GajuFormat.Type.valueOf(appState.balanceFormat)
|
||||
val unit = GajuFormat.Unit.valueOf(appState.balanceUnit)
|
||||
GajuFormat.amount(GajuFormat.standardSpec(type, unit), result.amount)
|
||||
} else ""
|
||||
val initialPayload = if (result?.payload != null) String(result.payload, Charsets.UTF_8) else ""
|
||||
|
||||
SendFormScreen(
|
||||
fromAccount = activeAccount,
|
||||
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
|
||||
balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit),
|
||||
initialRecipient = initialRecipient,
|
||||
initialAmount = initialAmount,
|
||||
initialPayload = initialPayload,
|
||||
onReview = { recipient, amountStr, payload, gas, gasPrice, ttl ->
|
||||
try {
|
||||
val pucks = GajuFormat.read(amountStr)
|
||||
val pucksBI = java.math.BigInteger(pucks)
|
||||
onError("Reviewing: To $recipient, $pucksBI Pucks, Msg: $payload, Gas: $gas, Price: $gasPrice, TTL: $ttl")
|
||||
} catch (e: Exception) {
|
||||
onError("Invalid amount: ${e.message}")
|
||||
}
|
||||
},
|
||||
onBack = { navigateBack() },
|
||||
)
|
||||
}
|
||||
Screen.QR_RECEIVE -> {
|
||||
val wallets = walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) }
|
||||
val activeWallet = wallets.getOrNull(activeWalletIndex)
|
||||
val activeAccount = activeWallet?.accounts?.getOrNull(activeAccountIndex)
|
||||
|
||||
if (activeAccount == null) {
|
||||
onError("No active account selected.")
|
||||
navigateBack()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
QRReceiveScreen(
|
||||
account = activeAccount,
|
||||
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
|
||||
balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit),
|
||||
onDone = { navigateBack() },
|
||||
)
|
||||
}
|
||||
Screen.EXPLORER -> {
|
||||
ExplorerScreen(
|
||||
onNavigate = { dest ->
|
||||
val screen = when (dest) {
|
||||
"dashboard" -> Screen.DASHBOARD
|
||||
"settings" -> Screen.SETTINGS
|
||||
"manage_wallets" -> Screen.MANAGE_WALLETS
|
||||
else -> null
|
||||
}
|
||||
if (screen != null) {
|
||||
screenStack.clear()
|
||||
screenStack.add(screen)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ data class Account(
|
||||
*/
|
||||
fun formattedBalance(type: GajuFormat.Type, unit: GajuFormat.Unit): String {
|
||||
val bi = try { BigInteger(cachedBalance?.totalPucks ?: "0") } catch (_: Exception) { BigInteger.ZERO }
|
||||
val spec = GajuFormat.FormatSpec(type, unit, ',', 3)
|
||||
return GajuFormat.amount(spec, bi)
|
||||
return GajuFormat.amount(GajuFormat.standardSpec(type, unit), bi)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -2,11 +2,30 @@ package swiss.qpq.gajumobile.data.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Endpoint(
|
||||
val host: String,
|
||||
val port: Int = 3013,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Wallet(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val networkId: String = "groot.mainnet",
|
||||
val accounts: List<Account> = emptyList(),
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val network: String = "Mainnet"
|
||||
)
|
||||
val endpoints: List<Endpoint> = emptyList()
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT_MAINNET = listOf(
|
||||
Endpoint("groot.mainnet.gajumaru.io", 3013),
|
||||
Endpoint("tsuriai.jp", 3013)
|
||||
)
|
||||
|
||||
val DEFAULT_TESTNET = listOf(
|
||||
Endpoint("groot.testnet.gajumaru.io", 3013),
|
||||
Endpoint("tsuriai.jp", 4013)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ fun GajuButton(
|
||||
|
||||
@Composable
|
||||
fun GajuTopBar(
|
||||
onNavigate: (String) -> Unit
|
||||
onNavigate: (String) -> Unit,
|
||||
) {
|
||||
var menuExpanded by remember { mutableStateOf(value = false) }
|
||||
|
||||
@@ -260,6 +260,7 @@ fun GajuTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: String = "",
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
singleLine: Boolean = false,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
@@ -273,6 +274,7 @@ fun GajuTextField(
|
||||
onValueChange = onValueChange,
|
||||
placeholder = { Text(placeholder, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = singleLine,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
@@ -414,7 +416,7 @@ fun SecurityDataSection(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (index < data.size - 1) {
|
||||
if (index < (data.size - 1)) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package swiss.qpq.gajumobile.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -10,6 +11,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -27,10 +30,11 @@ import swiss.qpq.gajumobile.ui.components.GajuTextField
|
||||
|
||||
@Composable
|
||||
fun CreateWalletScreen(
|
||||
onWalletCreated: (String) -> Unit,
|
||||
onWalletCreated: (name: String, networkId: String) -> Unit,
|
||||
onBack: (() -> Unit)? = null
|
||||
) {
|
||||
var walletName by remember { mutableStateOf("") }
|
||||
var selectedNetwork by remember { mutableStateOf("groot.mainnet") }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -55,12 +59,41 @@ fun CreateWalletScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
GajuTextField(
|
||||
value = walletName,
|
||||
onValueChange = { walletName = it },
|
||||
label = "Wallet Name",
|
||||
placeholder = "e.g. Personal, Savings"
|
||||
placeholder = "e.g. Personal, Savings",
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
"NETWORK",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.align(Alignment.Start)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
listOf("groot.mainnet" to "Mainnet", "groot.testnet" to "Testnet").forEach { (id, label) ->
|
||||
val isSelected = selectedNetwork == id
|
||||
AssistChip(
|
||||
onClick = { selectedNetwork = id },
|
||||
label = { Text(label) },
|
||||
colors = AssistChipDefaults.assistChipColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
labelColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
@@ -77,7 +110,7 @@ fun CreateWalletScreen(
|
||||
GajuButton(
|
||||
text = "Continue",
|
||||
enabled = walletName.isNotBlank(),
|
||||
onClick = { onWalletCreated(walletName) },
|
||||
onClick = { onWalletCreated(walletName, selectedNetwork) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ fun EnvironmentInfoScreen(
|
||||
title = "GajuMobile",
|
||||
data = listOf(
|
||||
"App Version" to appVersion,
|
||||
"Network" to "Mainnet (Production)"
|
||||
"Theme" to "Arboreal (Gajumaru)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -34,6 +35,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import swiss.qpq.gajumobile.R
|
||||
import swiss.qpq.gajumobile.data.models.Endpoint
|
||||
import swiss.qpq.gajumobile.data.models.Wallet
|
||||
import swiss.qpq.gajumobile.ui.components.GajuHeader
|
||||
import swiss.qpq.gajumobile.ui.components.GajuTextField
|
||||
@@ -45,18 +47,22 @@ fun ManageWalletsScreen(
|
||||
onAddWallet: () -> Unit,
|
||||
onRenameWallet: (String, String) -> Unit,
|
||||
onDeleteWallet: (String) -> Unit,
|
||||
onUpdateNetwork: (String, String) -> Unit,
|
||||
onBack: () -> Unit
|
||||
onUpdateEndpoints: (String, List<Endpoint>) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var walletToRename by remember { mutableStateOf<Wallet?>(null) }
|
||||
var newName by remember { mutableStateOf("") }
|
||||
|
||||
var walletForNewEndpoint by remember { mutableStateOf<Wallet?>(null) }
|
||||
var newHost by remember { mutableStateOf("") }
|
||||
var newPort by remember { mutableStateOf("3013") }
|
||||
|
||||
if (walletToRename != null) {
|
||||
androidx.compose.ui.window.Dialog(onDismissRequest = { walletToRename = null }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 8.dp
|
||||
tonalElevation = 8.dp,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Text("Rename Wallet", style = MaterialTheme.typography.headlineSmall)
|
||||
@@ -70,10 +76,60 @@ fun ManageWalletsScreen(
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = { walletToRename = null }) { Text("CANCEL") }
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(onClick = {
|
||||
walletToRename?.let { onRenameWallet(it.id, newName) }
|
||||
walletToRename = null
|
||||
}) { Text("SAVE") }
|
||||
TextButton(
|
||||
onClick = {
|
||||
walletToRename?.let { onRenameWallet(it.id, newName) }
|
||||
walletToRename = null
|
||||
}
|
||||
) { Text("SAVE") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (walletForNewEndpoint != null) {
|
||||
androidx.compose.ui.window.Dialog(onDismissRequest = { walletForNewEndpoint = null }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 8.dp,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Text("Add Endpoint", style = MaterialTheme.typography.headlineSmall)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
GajuTextField(
|
||||
value = newHost,
|
||||
onValueChange = { newHost = it },
|
||||
label = "Host (IP or Domain)",
|
||||
singleLine = true,
|
||||
placeholder = "groot.gajumaru.io"
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
GajuTextField(
|
||||
value = newPort,
|
||||
onValueChange = { newPort = it },
|
||||
label = "Port",
|
||||
singleLine = true,
|
||||
placeholder = "3013"
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = { walletForNewEndpoint = null }) { Text("CANCEL") }
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(
|
||||
enabled = (newHost.isNotBlank() && newPort.toIntOrNull() != null),
|
||||
onClick = {
|
||||
walletForNewEndpoint?.let { wallet ->
|
||||
val port = newPort.toIntOrNull() ?: 3013
|
||||
val newList = wallet.endpoints + Endpoint(newHost, port)
|
||||
onUpdateEndpoints(wallet.id, newList)
|
||||
}
|
||||
walletForNewEndpoint = null
|
||||
newHost = ""
|
||||
newPort = "3013"
|
||||
}
|
||||
) { Text("ADD") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +163,11 @@ fun ManageWalletsScreen(
|
||||
walletToRename = wallet
|
||||
},
|
||||
onDelete = { onDeleteWallet(wallet.id) },
|
||||
onUpdateNetwork = { onUpdateNetwork(wallet.id, it) }
|
||||
onAddEndpoint = { walletForNewEndpoint = wallet },
|
||||
onDeleteEndpoint = { index ->
|
||||
val newList = wallet.endpoints.toMutableList().apply { removeAt(index) }
|
||||
onUpdateEndpoints(wallet.id, newList)
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
@@ -134,50 +194,90 @@ private fun WalletManagementCard(
|
||||
wallet: Wallet,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onUpdateNetwork: (String) -> Unit
|
||||
onAddEndpoint: () -> Unit,
|
||||
onDeleteEndpoint: (Int) -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = wallet.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable { onRename() }
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = wallet.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.clickable { onRename() },
|
||||
)
|
||||
Text(
|
||||
text = wallet.networkId.uppercase(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_delete),
|
||||
contentDescription = "Delete Wallet",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
GajuTextField(
|
||||
value = wallet.network,
|
||||
onValueChange = onUpdateNetwork,
|
||||
label = "Network",
|
||||
placeholder = "e.g. Mainnet, Testnet"
|
||||
Text(
|
||||
text = "NETWORK ENDPOINTS",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
wallet.endpoints.forEachIndexed { index, endpoint ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "${endpoint.host}:${endpoint.port}",
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (wallet.endpoints.size > 1) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_delete),
|
||||
contentDescription = "Remove Endpoint",
|
||||
modifier = Modifier.size(16.dp).clickable { onDeleteEndpoint(index) },
|
||||
tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onAddEndpoint,
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Icon(painterResource(R.drawable.ic_add), null, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("ADD ENDPOINT", fontSize = 11.sp)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
|
||||
Text(
|
||||
text = "${wallet.accounts.size} account(s)",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package swiss.qpq.gajumobile.ui.screens
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.foundation.Canvas
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -13,41 +18,109 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import swiss.qpq.gajumobile.R
|
||||
import swiss.qpq.gajumobile.ui.components.GajuButton
|
||||
import swiss.qpq.gajumobile.ui.components.GajuHeader
|
||||
import swiss.qpq.gajumobile.ui.components.GajuTextField
|
||||
import swiss.qpq.gajumaru.core.encoding.QR
|
||||
import swiss.qpq.gajumaru.core.formatting.GajuFormat
|
||||
import java.math.BigInteger
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Composable
|
||||
fun QRReceiveScreen(
|
||||
onDone: () -> Unit
|
||||
account: swiss.qpq.gajumobile.data.models.Account,
|
||||
balanceFormat: GajuFormat.Type,
|
||||
balanceUnit: GajuFormat.Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
var amountStr by remember { mutableStateOf(value = "") }
|
||||
var payload by remember { mutableStateOf(value = "") }
|
||||
var isGenerated by remember { mutableStateOf(value = false) }
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val amountPucks = remember(amountStr) {
|
||||
try {
|
||||
if (amountStr.isBlank()) BigInteger.ZERO else BigInteger(GajuFormat.read(amountStr))
|
||||
} catch (_: Exception) {
|
||||
BigInteger.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
val formattedAmount = remember(amountPucks, balanceFormat, balanceUnit) {
|
||||
GajuFormat.amount(GajuFormat.standardSpec(balanceFormat, balanceUnit), amountPucks)
|
||||
}
|
||||
|
||||
val generatedUrl = remember(amountPucks, payload, account.gajuId) {
|
||||
try {
|
||||
val baseUrl = "grids://gaju/1/s/${account.gajuId}?a=$amountPucks"
|
||||
if (payload.isNotBlank()) {
|
||||
val p = URLEncoder.encode(payload, "UTF-8")
|
||||
"$baseUrl&p=$p"
|
||||
} else {
|
||||
baseUrl
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
val qrMatrix = remember(generatedUrl) {
|
||||
if (generatedUrl.isNotEmpty()) {
|
||||
try { QR.encode(generatedUrl) } catch (_: Exception) { null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
Column(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
.padding(24.dp),
|
||||
) {
|
||||
GajuButton(
|
||||
text = "DONE",
|
||||
onClick = onDone,
|
||||
modifier = Modifier.width(160.dp),
|
||||
text = if (isGenerated) "EDIT" else "BACK",
|
||||
onClick = { if (isGenerated) isGenerated = false else onDone() },
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = MaterialTheme.colorScheme.secondary,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
GajuButton(
|
||||
text = if (isGenerated) "DONE" else "GENERATE",
|
||||
onClick = { if (isGenerated) onDone() else isGenerated = true },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -57,42 +130,200 @@ fun QRReceiveScreen(
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 24.dp),
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
GajuHeader()
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = if (isGenerated) "TRANSFER REQUEST" else "RECEIVE",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
)
|
||||
|
||||
// QR Code Placeholder
|
||||
Surface(
|
||||
modifier = Modifier.size(240.dp),
|
||||
color = Color.White, // QR Codes are usually black on white for scanning
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (!isGenerated) {
|
||||
// Input Phase
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("RECIPIENT", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = account.label,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Text(
|
||||
text = account.gajuId,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Amount (String input)
|
||||
val unitLabel = if (balanceUnit == GajuFormat.Unit.GAJU) "GAJU" else "PUCK"
|
||||
GajuTextField(
|
||||
value = amountStr,
|
||||
onValueChange = { amountStr = it },
|
||||
label = "REQUESTED AMOUNT (OPTIONAL)",
|
||||
placeholder = "0.0",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = { Text(unitLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
GajuTextField(
|
||||
value = payload,
|
||||
onValueChange = { payload = it },
|
||||
label = "PAYLOAD / MESSAGE (OPTIONAL)",
|
||||
placeholder = "Add a note...",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
// Display Phase
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp))
|
||||
.padding(16.dp),
|
||||
) {
|
||||
DetailRow(label = "RECIPIENT", value = account.label)
|
||||
Text(
|
||||
text = account.gajuId,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
DetailRow(label = "AMOUNT", value = formattedAmount)
|
||||
if (payload.isNotBlank()) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
DetailRow(label = "MESSAGE", value = payload)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.size(280.dp),
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(16.dp)) {
|
||||
if (qrMatrix != null) {
|
||||
QRCanvas(qrMatrix)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_qr_code),
|
||||
contentDescription = "QR Code",
|
||||
modifier = Modifier.size(180.dp),
|
||||
tint = Color.Black.copy(alpha = 0.1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = "GRIDS URL",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_qr_code),
|
||||
contentDescription = "QR Code",
|
||||
modifier = Modifier.size(180.dp),
|
||||
tint = Color.Black,
|
||||
painter = painterResource(id = R.drawable.ic_content_copy),
|
||||
contentDescription = "Copy URL",
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("GRIDS URL", generatedUrl)))
|
||||
}
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = generatedUrl,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Scan to send to this account",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailRow(label: String, value: String) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f))
|
||||
Text(text = value, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QRCanvas(matrix: Array<BooleanArray>) {
|
||||
val size = matrix.size
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val cellSize = this.size.width / size
|
||||
val offsetX = (this.size.width - (cellSize * size)) / 2
|
||||
val offsetY = (this.size.height - (cellSize * size)) / 2
|
||||
|
||||
for (y in 0 until size) {
|
||||
for (x in 0 until size) {
|
||||
if (matrix[y][x]) {
|
||||
drawRect(
|
||||
color = Color.Black,
|
||||
topLeft = androidx.compose.ui.geometry.Offset(offsetX + (x * cellSize), offsetY + (y * cellSize)),
|
||||
size = androidx.compose.ui.geometry.Size(cellSize, cellSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Sender must scan this QR.",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
)
|
||||
Text(
|
||||
text = "You can also send them a screenshot of this QR.",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package swiss.qpq.gajumobile.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -9,30 +10,58 @@ 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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
|
||||
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
|
||||
import swiss.qpq.gajumobile.R
|
||||
import swiss.qpq.gajumobile.ui.components.GajuHeader
|
||||
import swiss.qpq.gajumobile.ui.components.ScannerAction
|
||||
import swiss.qpq.gajumobile.ui.components.GajuButton
|
||||
|
||||
@Composable
|
||||
fun QRScannerScreen(
|
||||
onScanResult: (String) -> Unit,
|
||||
onGenerate: () -> Unit,
|
||||
onUpload: () -> Unit
|
||||
onUpload: () -> Unit,
|
||||
onManualEntry: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val scannerOptions = GmsBarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.enableAutoZoom()
|
||||
.build()
|
||||
|
||||
val scanner = GmsBarcodeScanning.getClient(context, scannerOptions)
|
||||
|
||||
fun startScan() {
|
||||
scanner.startScan()
|
||||
.addOnSuccessListener { barcode ->
|
||||
barcode.rawValue?.let { onScanResult(it) }
|
||||
}
|
||||
.addOnFailureListener {
|
||||
// Handle error if needed
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -55,21 +84,41 @@ fun QRScannerScreen(
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Scanner Viewfinder Mockup
|
||||
// Scanner Prompt Area
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(280.dp)
|
||||
.border(2.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(16.dp)),
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(16.dp))
|
||||
.clickable { startScan() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_qr_code),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_qr_code),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "TAP TO SCAN",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
|
||||
GajuButton(
|
||||
text = "MANUAL ENTRY",
|
||||
onClick = onManualEntry,
|
||||
modifier = Modifier.padding(horizontal = 48.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
@@ -78,6 +127,7 @@ fun QRScannerScreen(
|
||||
.padding(bottom = 48.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
ScannerAction(label = "Back", icon = R.drawable.ic_home, onClick = onBack)
|
||||
ScannerAction(label = "Generate QR", icon = R.drawable.ic_qr_code, onClick = onGenerate)
|
||||
ScannerAction(label = "Upload QR", icon = R.drawable.ic_qr_code, onClick = onUpload)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package swiss.qpq.gajumobile.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -13,36 +14,47 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
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.foundation.layout.size
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import swiss.qpq.gajumobile.R
|
||||
import swiss.qpq.gajumobile.ui.components.GajuButton
|
||||
import swiss.qpq.gajumobile.ui.components.GajuHeader
|
||||
import swiss.qpq.gajumobile.ui.components.GajuTextField
|
||||
import swiss.qpq.gajumaru.core.formatting.GajuFormat
|
||||
|
||||
@Composable
|
||||
fun SendFormScreen(
|
||||
onReview: () -> Unit,
|
||||
fromAccount: swiss.qpq.gajumobile.data.models.Account,
|
||||
balanceFormat: GajuFormat.Type,
|
||||
balanceUnit: GajuFormat.Unit,
|
||||
initialRecipient: String = "",
|
||||
initialAmount: String = "",
|
||||
initialPayload: String = "",
|
||||
onReview: (recipient: String, amount: String, payload: String, gas: String, gasPrice: String, ttl: String) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var toAddress by remember { mutableStateOf(value = "") }
|
||||
var amount by remember { mutableStateOf(value = "") }
|
||||
var toAddress by remember { mutableStateOf(value = initialRecipient) }
|
||||
var amount by remember { mutableStateOf(value = initialAmount) }
|
||||
var payload by remember { mutableStateOf(value = initialPayload) }
|
||||
var gas by remember { mutableStateOf(value = "20000") } // Default gas
|
||||
var gasPrice by remember { mutableStateOf(value = "1000000000") } // Default gas price
|
||||
var ttl by remember { mutableStateOf(value = "500") } // Default TTL
|
||||
var showAdvanced by remember { mutableStateOf(value = false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -63,8 +75,9 @@ fun SendFormScreen(
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
GajuButton(
|
||||
text = "REVIEW",
|
||||
onClick = onReview,
|
||||
onClick = { onReview(toAddress, amount, payload, gas, gasPrice, ttl) },
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = toAddress.isNotBlank() && amount.isNotBlank(),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -88,48 +101,125 @@ fun SendFormScreen(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
// 0. Sender Account (Read-only)
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("SENDING FROM", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Daily", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold)
|
||||
Icon(painterResource(R.drawable.gajumobile_icon_colored), null, tint = Color.Unspecified, modifier = Modifier.size(16.dp)) // Dropdown arrow replacement
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = fromAccount.label,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Text(
|
||||
text = "Balance: ${fromAccount.formattedBalance(balanceFormat, balanceUnit)}",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
text = fromAccount.gajuId,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "Primary Wallet",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 1. Recipient Account ID
|
||||
GajuTextField(
|
||||
value = toAddress,
|
||||
onValueChange = { toAddress = it },
|
||||
label = "SENDING TO",
|
||||
label = "RECIPIENT ACCOUNT",
|
||||
placeholder = "ak_...",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 2. Amount (String input)
|
||||
val unitLabel = if (balanceUnit == GajuFormat.Unit.GAJU) "GAJU" else "PUCK"
|
||||
GajuTextField(
|
||||
value = amount,
|
||||
onValueChange = { amount = it },
|
||||
label = "AMOUNT",
|
||||
placeholder = "0.0",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = { Text("木", color = MaterialTheme.colorScheme.onSurface) },
|
||||
trailingIcon = { Text(unitLabel, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("CURRENCY", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("木 Gajus", color = MaterialTheme.colorScheme.onBackground)
|
||||
Icon(painterResource(R.drawable.gajumobile_icon_colored), null, tint = Color.Unspecified, modifier = Modifier.size(16.dp))
|
||||
// 3. Payload/Message
|
||||
GajuTextField(
|
||||
value = payload,
|
||||
onValueChange = { payload = it },
|
||||
label = "PAYLOAD / MESSAGE",
|
||||
placeholder = "Optional message...",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = if (showAdvanced) "HIDE DETAILS" else "SHOW DETAILS",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showAdvanced = !showAdvanced }
|
||||
.padding(vertical = 8.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
if (showAdvanced) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 4. Gas
|
||||
GajuTextField(
|
||||
value = gas,
|
||||
onValueChange = { gas = it },
|
||||
label = "GAS",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
// 5. Gas Price
|
||||
GajuTextField(
|
||||
value = gasPrice,
|
||||
onValueChange = { gasPrice = it },
|
||||
label = "GAS PRICE",
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
// 6. TTL
|
||||
GajuTextField(
|
||||
value = ttl,
|
||||
onValueChange = { ttl = it },
|
||||
label = "TTL",
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,31 @@ package swiss.qpq.gajumobile.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// Neon Dark Palette
|
||||
val NeonDarkBackground = Color(0xFF1C1B1F)
|
||||
val NeonDarkSurface = Color(0xFF252429)
|
||||
val NeonDarkSurfaceVariant = Color(0xFF121212)
|
||||
val NeonDarkPrimary = Color(0xFF9F72FF)
|
||||
val NeonDarkSecondary = Color(0xFFE0E0E0)
|
||||
val NeonDarkAccent = Color(0xFF00E5FF)
|
||||
val NeonDarkOnPrimary = Color(0xFFFFFFFF)
|
||||
val NeonDarkOnSecondary = Color(0xFF000000)
|
||||
val NeonDarkOnBackground = Color(0xFFFFFFFF)
|
||||
val NeonDarkOnSurface = Color(0xFFFFFFFF)
|
||||
val NeonDarkOnSurfaceVariant = Color(0xFFE0E0E0)
|
||||
// Arboreal Dark Palette (Gajumaru Theme)
|
||||
val GajuDarkBackground = Color(0xFF1B1C17) // Dark bark/olive
|
||||
val GajuDarkSurface = Color(0xFF23251D)
|
||||
val GajuDarkSurfaceVariant = Color(0xFF181A12)
|
||||
val GajuDarkPrimary = Color(0xFF81C784) // Fresh leaf green
|
||||
val GajuDarkSecondary = Color(0xFFA1887F) // Light bark brown
|
||||
val GajuDarkAccent = Color(0xFFD4E157) // Moss yellow-green
|
||||
val GajuDarkOnPrimary = Color(0xFF003300)
|
||||
val GajuDarkOnSecondary = Color(0xFF3E2723)
|
||||
val GajuDarkOnBackground = Color(0xFFE3E3DC)
|
||||
val GajuDarkOnSurface = Color(0xFFE3E3DC)
|
||||
val GajuDarkOnSurfaceVariant = Color(0xFFC7C7BC)
|
||||
|
||||
// Classic Light Palette
|
||||
val ClassicLightBackground = Color(0xFFFFFFFF)
|
||||
val ClassicLightSurface = Color(0xFFF5F5F5)
|
||||
val ClassicLightSurfaceVariant = Color(0xFFEEEEEE)
|
||||
val ClassicLightPrimary = Color(0xFF6200EE)
|
||||
val ClassicLightSecondary = Color(0xFF757575)
|
||||
val ClassicLightOnPrimary = Color(0xFFFFFFFF)
|
||||
val ClassicLightOnSecondary = Color(0xFFFFFFFF)
|
||||
val ClassicLightOnBackground = Color(0xFF000000)
|
||||
val ClassicLightOnSurface = Color(0xFF000000)
|
||||
val ClassicLightOnSurfaceVariant = Color(0xFF424242)
|
||||
// Arboreal Light Palette
|
||||
val GajuLightBackground = Color(0xFFFCFCF5)
|
||||
val GajuLightSurface = Color(0xFFF1F1EA)
|
||||
val GajuLightSurfaceVariant = Color(0xFFE3E3DC)
|
||||
val GajuLightPrimary = Color(0xFF2E7D32) // Forest green
|
||||
val GajuLightSecondary = Color(0xFF5D4037) // Deep wood brown
|
||||
val GajuLightOnPrimary = Color(0xFFFFFFFF)
|
||||
val GajuLightOnSecondary = Color(0xFFFFFFFF)
|
||||
val GajuLightOnBackground = Color(0xFF1B1C17)
|
||||
val GajuLightOnSurface = Color(0xFF1B1C17)
|
||||
val GajuLightOnSurfaceVariant = Color(0xFF46483C)
|
||||
|
||||
// Logo Gradient Colors (approximate from image)
|
||||
val GajuLogoPurple = Color(0xFF9D39FF)
|
||||
val GajuLogoTeal = Color(0xFF39FFEA)
|
||||
// Logo Gradient Colors (Arboreal Shift)
|
||||
val GajuLogoGreen = Color(0xFF4CAF50)
|
||||
val GajuLogoBrown = Color(0xFF795548)
|
||||
|
||||
@@ -10,31 +10,31 @@ import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val NeonDarkColorScheme = darkColorScheme(
|
||||
primary = NeonDarkPrimary,
|
||||
secondary = NeonDarkSecondary,
|
||||
tertiary = NeonDarkAccent,
|
||||
background = NeonDarkBackground,
|
||||
surface = NeonDarkSurface,
|
||||
surfaceVariant = NeonDarkSurfaceVariant,
|
||||
onPrimary = NeonDarkOnPrimary,
|
||||
onSecondary = NeonDarkOnSecondary,
|
||||
onBackground = NeonDarkOnBackground,
|
||||
onSurface = NeonDarkOnSurface,
|
||||
onSurfaceVariant = NeonDarkOnSurfaceVariant,
|
||||
private val ArborealDarkColorScheme = darkColorScheme(
|
||||
primary = GajuDarkPrimary,
|
||||
secondary = GajuDarkSecondary,
|
||||
tertiary = GajuDarkAccent,
|
||||
background = GajuDarkBackground,
|
||||
surface = GajuDarkSurface,
|
||||
surfaceVariant = GajuDarkSurfaceVariant,
|
||||
onPrimary = GajuDarkOnPrimary,
|
||||
onSecondary = GajuDarkOnSecondary,
|
||||
onBackground = GajuDarkOnBackground,
|
||||
onSurface = GajuDarkOnSurface,
|
||||
onSurfaceVariant = GajuDarkOnSurfaceVariant,
|
||||
)
|
||||
|
||||
private val ClassicLightColorScheme = lightColorScheme(
|
||||
primary = ClassicLightPrimary,
|
||||
secondary = ClassicLightSecondary,
|
||||
background = ClassicLightBackground,
|
||||
surface = ClassicLightSurface,
|
||||
surfaceVariant = ClassicLightSurfaceVariant,
|
||||
onPrimary = ClassicLightOnPrimary,
|
||||
onSecondary = ClassicLightOnSecondary,
|
||||
onBackground = ClassicLightOnBackground,
|
||||
onSurface = ClassicLightOnSurface,
|
||||
onSurfaceVariant = ClassicLightOnSurfaceVariant,
|
||||
private val ArborealLightColorScheme = lightColorScheme(
|
||||
primary = GajuLightPrimary,
|
||||
secondary = GajuLightSecondary,
|
||||
background = GajuLightBackground,
|
||||
surface = GajuLightSurface,
|
||||
surfaceVariant = GajuLightSurfaceVariant,
|
||||
onPrimary = GajuLightOnPrimary,
|
||||
onSecondary = GajuLightOnSecondary,
|
||||
onBackground = GajuLightOnBackground,
|
||||
onSurface = GajuLightOnSurface,
|
||||
onSurfaceVariant = GajuLightOnSurfaceVariant,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -42,7 +42,7 @@ fun MyApplicationTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = if (darkTheme) NeonDarkColorScheme else ClassicLightColorScheme
|
||||
val colorScheme = if (darkTheme) ArborealDarkColorScheme else ArborealLightColorScheme
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
|
||||
+1
-1
Submodule gm-java updated: 7493b473a7...11516b1cca
@@ -8,6 +8,7 @@ lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
kotlin = "2.2.10"
|
||||
composeBom = "2025.12.00"
|
||||
playServicesCodeScanner = "16.1.0"
|
||||
coreSplashScreen = "1.2.0"
|
||||
biometric = "1.1.0"
|
||||
lifecycleProcess = "2.6.1"
|
||||
@@ -31,6 +32,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat
|
||||
androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
|
||||
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashScreen" }
|
||||
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
|
||||
play-services-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "playServicesCodeScanner" }
|
||||
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user