This commit is contained in:
2026-08-23 16:13:57 +09:00
parent 982d176504
commit 27c6e1c521
16 changed files with 806 additions and 188 deletions
+1
View File
@@ -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)
+5
View File
@@ -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
+2
View File
@@ -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" }