Fix keyboard padding and better currency management

This commit is contained in:
soraefir
2026-06-29 21:27:27 +02:00
parent ec4d60897a
commit 7e00b14d27
7 changed files with 248 additions and 104 deletions
@@ -117,6 +117,7 @@ fun EditBillScreen(
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.fillMaxSize()
.verticalScroll(scrollState)
@@ -67,8 +67,10 @@ class ManageCurrenciesActivity : AppCompatActivity() {
viewModel = viewModel,
onBack = { finish() },
onSaveMain = { saveMainCurrency() },
onAdd = { addCurrency() },
onDelete = { deleteCurrency(it) }
onAdd = { addOrUpdateCurrency() },
onDelete = { deleteCurrency(it) },
onEdit = { startEditing(it) },
onCancelEdit = { cancelEditing() }
)
}
}
@@ -105,22 +107,44 @@ class ManageCurrenciesActivity : AppCompatActivity() {
}
}
private fun addCurrency() {
private fun addOrUpdateCurrency() {
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
val newCurrency = DBCurrency(
0, 0, selectedProjectID,
viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
)
val currencyName = viewModel.newCurrencyName
val editingId = viewModel.editingCurrencyId
lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (editingId != null) {
db!!.updateCurrency(editingId, currencyName, exchangeRate)
val currency = db!!.getCurrency(editingId)
if (currency != null) {
db!!.setCurrencyStateSync(editingId, DBBill.STATE_EDITED)
}
} else {
val newCurrency = DBCurrency(
0, 0, selectedProjectID,
currencyName, exchangeRate, DBBill.STATE_ADDED
)
db!!.addCurrencyAndSync(newCurrency)
}
viewModel.newCurrencyName = ""
viewModel.newCurrencyRate = ""
}
cancelEditing()
updateCurrenciesList()
}
}
private fun startEditing(currency: DBCurrency) {
viewModel.editingCurrencyId = currency.id
viewModel.newCurrencyName = currency.name ?: ""
viewModel.newCurrencyRate = currency.exchangeRate.toString()
}
private fun cancelEditing() {
viewModel.editingCurrencyId = null
viewModel.newCurrencyName = ""
viewModel.newCurrencyRate = ""
}
private fun deleteCurrency(currency: DBCurrency) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
@@ -1,6 +1,9 @@
package net.helcel.cowspent.android.currencies
import android.annotation.SuppressLint
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -8,16 +11,25 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.AlertDialog
import net.helcel.cowspent.android.helper.TextIcon
import net.helcel.cowspent.android.helper.TextIconDisplay
import net.helcel.cowspent.android.helper.formatAmount
import net.helcel.cowspent.model.DBCurrency
@@ -27,7 +39,9 @@ fun ManageCurrenciesScreen(
onBack: () -> Unit,
onSaveMain: () -> Unit,
onAdd: () -> Unit,
onDelete: (DBCurrency) -> Unit
onDelete: (DBCurrency) -> Unit,
onEdit: (DBCurrency) -> Unit,
onCancelEdit: () -> Unit
) {
val dialogState = viewModel.dialogState
if (dialogState != null) {
@@ -76,78 +90,152 @@ fun ManageCurrenciesScreen(
Column(
modifier = Modifier
.padding(padding)
.padding(16.dp)
.imePadding()
.fillMaxSize()
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.02f))
) {
LaunchedEffect(viewModel.mainCurrencyName) {
if (viewModel.mainCurrencyName.isNotEmpty()) {
delay(500)
onSaveMain()
}
}
Column(modifier = Modifier.padding(16.dp)) {
// Section 1: Main Currency
Text(
text = stringResource(R.string.main_currency).uppercase(),
style = MaterialTheme.typography.overline,
color = MaterialTheme.colors.primary,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Card(
shape = RoundedCornerShape(12.dp),
elevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6)
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = viewModel.mainCurrencyName,
onValueChange = { viewModel.mainCurrencyName = it },
label = { Text(stringResource(R.string.currency_edit_name)) },
modifier = Modifier.weight(1f)
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("e.g. EUR", fontSize = 14.sp) },
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = onSaveMain, enabled = viewModel.mainCurrencyName.isNotEmpty()) {
Text(stringResource(R.string.save_or_discard_bill_dialog_save))
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
// Section 2: Exchange Rates
val isEditing = viewModel.editingCurrencyId != null
Text(
text = (if (isEditing) "Edit exchange rate" else "Add exchange rate").uppercase(),
style = MaterialTheme.typography.overline,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
// Visual relationship indicator
// Integrated Add/Edit Ribbon
Surface(
color = MaterialTheme.colors.primary.copy(alpha = 0.05f),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
color = if (isEditing) MaterialTheme.colors.secondary.copy(alpha = 0.08f) else MaterialTheme.colors.surface,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth(),
elevation = if (isEditing) 4.dp else 2.dp,
border = if (isEditing) BorderStroke(2.dp, MaterialTheme.colors.secondary) else null
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("1 ", style = MaterialTheme.typography.body1)
Text("1 ", style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontWeight = FontWeight.Bold)
Text(" = ", style = MaterialTheme.typography.h6)
Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary)
Text(" ")
Text(viewModel.newCurrencyName.ifEmpty { "Currency" }, fontWeight = FontWeight.Bold)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = viewModel.newCurrencyName,
onValueChange = { viewModel.newCurrencyName = it },
label = { Text(stringResource(R.string.currency_edit_name)) },
modifier = Modifier.weight(1f),
placeholder = { Text("e.g. USD") }
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 4.dp)
)
Spacer(modifier = Modifier.width(8.dp))
OutlinedTextField(
value = viewModel.newCurrencyRate,
onValueChange = { viewModel.newCurrencyRate = it },
label = { Text(stringResource(R.string.currency_rate)) },
modifier = Modifier.weight(1f),
placeholder = { Text("1.0") }
modifier = Modifier.weight(1.2f).height(52.dp),
placeholder = { Text("Rate", fontSize = 12.sp) },
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.ExtraBold, fontSize = 14.sp),
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.02f)
)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
)
Spacer(modifier = Modifier.width(8.dp))
OutlinedTextField(
value = viewModel.newCurrencyName,
onValueChange = { viewModel.newCurrencyName = it },
modifier = Modifier.weight(1f).height(52.dp),
placeholder = { Text("Code", fontSize = 12.sp) },
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold, fontSize = 14.sp),
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.02f)
)
)
Row(modifier = Modifier.padding(start = 4.dp)) {
IconButton(
onClick = onAdd,
modifier = Modifier.fillMaxWidth(),
enabled = viewModel.isAddEnabled()
) {
Text(stringResource(R.string.simple_add))
Icon(
imageVector = if (isEditing) Icons.Default.Done else Icons.Default.Add,
contentDescription = null,
tint = if (viewModel.isAddEnabled()) {
if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
} else MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
)
}
Spacer(modifier = Modifier.height(24.dp))
if (isEditing) {
IconButton(onClick = onCancelEdit) {
Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.error)
}
}
}
}
}
LazyColumn(modifier = Modifier.weight(1f)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "SAVED RATES",
style = MaterialTheme.typography.overline,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f),
fontWeight = FontWeight.Bold
)
}
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(viewModel.currencies) { currency ->
CurrencyRow(currency, viewModel.mainCurrencyName, onDelete = { onDelete(currency) })
CurrencyRow(
currency = currency,
mainCurrencyName = viewModel.mainCurrencyName,
isEditing = viewModel.editingCurrencyId == currency.id,
onEdit = { onEdit(currency) },
onDelete = { onDelete(currency) }
)
}
}
}
@@ -155,19 +243,33 @@ fun ManageCurrenciesScreen(
}
@Composable
fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> Unit) {
fun CurrencyRow(
currency: DBCurrency,
mainCurrencyName: String,
isEditing: Boolean,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
Card(
shape = RoundedCornerShape(12.dp),
elevation = if (isEditing) 4.dp else 1.dp,
border = if (isEditing) BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.5f)) else null,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onEdit)
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Table-like layout
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "1 ",
style = MaterialTheme.typography.body1,
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Text(
@@ -178,29 +280,33 @@ fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () ->
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.primary,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 8.dp)
)
Column {
Text(
text = formatAmount(currency.exchangeRate),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.ExtraBold
fontWeight = FontWeight.ExtraBold,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
)
Text(
text = currency.name ?: "",
style = MaterialTheme.typography.caption,
text = " ${currency.name ?: ""}",
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error)
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colors.error.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp)
)
}
}
}
Divider()
}
@Preview(showBackground = true)
@@ -210,6 +316,8 @@ fun CurrencyRowPreview() {
CurrencyRow(
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
mainCurrencyName = "EUR",
isEditing = false,
onEdit = {},
onDelete = {}
)
}
@@ -231,7 +339,9 @@ fun ManageCurrenciesScreenPreview() {
onBack = {},
onSaveMain = {},
onAdd = {},
onDelete = {}
onDelete = {},
onEdit = {},
onCancelEdit = {}
)
}
}
@@ -13,6 +13,8 @@ class ManageCurrenciesViewModel : ViewModel() {
var newCurrencyName by mutableStateOf("")
var newCurrencyRate by mutableStateOf("")
var editingCurrencyId by mutableStateOf<Long?>(null)
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
var dialogState by mutableStateOf<DialogState?>(null)
@@ -63,6 +63,7 @@ fun NewProjectScreen(
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.verticalScroll(rememberScrollState())
.fillMaxSize()
@@ -2,6 +2,8 @@ package net.helcel.cowspent.android.project.edit
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -80,11 +82,14 @@ fun EditProjectScreen(
}
}
) { padding ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.fillMaxSize()
.verticalScroll(scrollState)
) {
OutlinedTextField(
value = viewModel.name,
@@ -111,6 +111,7 @@ object ThemeUtils {
modifier = Modifier
.weight(1f)
.navigationBarsPadding()
.imePadding()
) {
content()
}