3 Commits

Author SHA1 Message Date
soraefir 84432fe68d Better formating and placeholder 2026-06-29 21:38:01 +02:00
soraefir 7e00b14d27 Fix keyboard padding and better currency management 2026-06-29 21:31:59 +02:00
soraefir ec4d60897a Currency parsing and formating
# Conflicts:
#	app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt
2026-06-29 21:30:35 +02:00
11 changed files with 329 additions and 121 deletions
@@ -187,7 +187,12 @@ class EditBillActivity : AppCompatActivity() {
} }
} }
calendar.timeInMillis = bill.timestamp * 1000 calendar.timeInMillis = bill.timestamp * 1000
val members = db.getMembersOfProject(bill.projectId, null) val allMembers = db.getMembersOfProject(bill.projectId, null)
val billOwerIds = bill.billOwersIds
val members = allMembers.filter { m ->
m.isActivated || m.id == bill.payerId || billOwerIds.contains(m.id)
}
val project = db.getProject(bill.projectId) val project = db.getProject(bill.projectId)
val currencies = db.getCurrencies(bill.projectId) val currencies = db.getCurrencies(bill.projectId)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -117,6 +117,7 @@ fun EditBillScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.imePadding()
.padding(16.dp) .padding(16.dp)
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState) .verticalScroll(scrollState)
@@ -188,7 +189,9 @@ fun BillBasicInfoSection(
placeholder = { Text("0") }, placeholder = { Text("0") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
leadingIcon = { leadingIcon = {
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { viewModel.mainCurrencyName } val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
viewModel.mainCurrencyName.ifEmpty { "$" }
}
TextIconDisplay( TextIconDisplay(
textIcon = TextIcon.Symbol(currencyToShow), textIcon = TextIcon.Symbol(currencyToShow),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
@@ -198,7 +201,7 @@ fun BillBasicInfoSection(
IconButton( IconButton(
enabled = canEdit, enabled = canEdit,
onClick = { onClick = {
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "Main" } val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" }
val options = listOf("$mainLabel | Base") + viewModel.currencies.map { val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}" "${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
} }
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.android.helper.DialogState import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.android.helper.parseAmount
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath import net.helcel.cowspent.util.evalMath
@@ -41,13 +42,8 @@ class EditBillViewModel : ViewModel() {
val amountAsDouble: Double val amountAsDouble: Double
get() { get() {
val amountStr = amount.replace(',', '.') return parseAmount(amount) ?: try {
return try { evalMath(amount.replace(',', '.'))
if (amountStr.matches("[0-9.]+".toRegex())) {
amountStr.toDouble()
} else {
evalMath(amountStr)
}
} catch (_: Exception) { } catch (_: Exception) {
0.0 0.0
} }
@@ -67,8 +67,10 @@ class ManageCurrenciesActivity : AppCompatActivity() {
viewModel = viewModel, viewModel = viewModel,
onBack = { finish() }, onBack = { finish() },
onSaveMain = { saveMainCurrency() }, onSaveMain = { saveMainCurrency() },
onAdd = { addCurrency() }, onAdd = { addOrUpdateCurrency() },
onDelete = { deleteCurrency(it) } 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 exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
val newCurrency = DBCurrency( val currencyName = viewModel.newCurrencyName
0, 0, selectedProjectID, val editingId = viewModel.editingCurrencyId
viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
)
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db!!.addCurrencyAndSync(newCurrency) 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 = "" cancelEditing()
viewModel.newCurrencyRate = ""
updateCurrenciesList() 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) { private fun deleteCurrency(currency: DBCurrency) {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -1,6 +1,9 @@
package net.helcel.cowspent.android.currencies package net.helcel.cowspent.android.currencies
import android.annotation.SuppressLint 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -8,16 +11,26 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack 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.Delete
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.R
import net.helcel.cowspent.android.helper.AlertDialog 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 import net.helcel.cowspent.model.DBCurrency
@Composable @Composable
@@ -26,7 +39,9 @@ fun ManageCurrenciesScreen(
onBack: () -> Unit, onBack: () -> Unit,
onSaveMain: () -> Unit, onSaveMain: () -> Unit,
onAdd: () -> Unit, onAdd: () -> Unit,
onDelete: (DBCurrency) -> Unit onDelete: (DBCurrency) -> Unit,
onEdit: (DBCurrency) -> Unit,
onCancelEdit: () -> Unit
) { ) {
val dialogState = viewModel.dialogState val dialogState = viewModel.dialogState
if (dialogState != null) { if (dialogState != null) {
@@ -75,78 +90,151 @@ fun ManageCurrenciesScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.padding(16.dp) .imePadding()
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.02f))
) { ) {
Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6) LaunchedEffect(viewModel.mainCurrencyName) {
Row(verticalAlignment = Alignment.CenterVertically) { if (viewModel.mainCurrencyName.isNotEmpty()) {
OutlinedTextField( delay(500)
value = viewModel.mainCurrencyName, onSaveMain()
onValueChange = { viewModel.mainCurrencyName = it },
label = { Text(stringResource(R.string.currency_edit_name)) },
modifier = Modifier.weight(1f)
)
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)) Column(modifier = Modifier.padding(16.dp)) {
// Section 1: Main Currency
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6) Text(
text = stringResource(R.string.main_currency).uppercase(),
// Visual relationship indicator style = MaterialTheme.typography.overline,
Surface( color = MaterialTheme.colors.primary,
color = MaterialTheme.colors.primary.copy(alpha = 0.05f), fontWeight = FontWeight.Bold
shape = RoundedCornerShape(8.dp), )
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) Spacer(modifier = Modifier.height(8.dp))
) { Card(
Row( shape = RoundedCornerShape(12.dp),
modifier = Modifier.padding(12.dp), elevation = 2.dp,
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()
horizontalArrangement = Arrangement.Center
) { ) {
Text("1 ", style = MaterialTheme.typography.body1) OutlinedTextField(
Text(viewModel.mainCurrencyName.ifEmpty { "Base" }, fontWeight = FontWeight.Bold) value = viewModel.mainCurrencyName,
Text(" = ", style = MaterialTheme.typography.h6) onValueChange = { viewModel.mainCurrencyName = it },
Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary) modifier = Modifier.fillMaxWidth(),
Text(" ") placeholder = { Text("e.g. EUR", fontSize = 14.sp) },
Text(viewModel.newCurrencyName.ifEmpty { "Currency" }, fontWeight = FontWeight.Bold) colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
} }
Spacer(modifier = Modifier.height(24.dp))
// 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))
// Integrated Add/Edit Ribbon
Surface(
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth(),
elevation = 2.dp
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("1 ", style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontWeight = FontWeight.Bold)
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 4.dp)
)
OutlinedTextField(
value = viewModel.newCurrencyRate,
onValueChange = { viewModel.newCurrencyRate = it },
modifier = Modifier.weight(1.2f).height(52.dp),
placeholder = { Text("1", 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.width(8.dp))
OutlinedTextField(
value = viewModel.newCurrencyName,
onValueChange = { viewModel.newCurrencyName = it },
modifier = Modifier.weight(1f).height(52.dp),
placeholder = { Text(viewModel.mainCurrencyName.ifEmpty { "$" }, 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,
enabled = viewModel.isAddEnabled()
) {
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)
)
}
if (isEditing) {
IconButton(onClick = onCancelEdit) {
Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.error)
}
}
}
}
}
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
)
} }
Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn(
OutlinedTextField( modifier = Modifier.weight(1f).fillMaxWidth(),
value = viewModel.newCurrencyName, contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
onValueChange = { viewModel.newCurrencyName = it }, verticalArrangement = Arrangement.spacedBy(8.dp)
label = { Text(stringResource(R.string.currency_edit_name)) },
modifier = Modifier.weight(1f),
placeholder = { Text("e.g. USD") }
)
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") }
)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onAdd,
modifier = Modifier.fillMaxWidth(),
enabled = viewModel.isAddEnabled()
) { ) {
Text(stringResource(R.string.simple_add))
}
Spacer(modifier = Modifier.height(24.dp))
LazyColumn(modifier = Modifier.weight(1f)) {
items(viewModel.currencies) { currency -> 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) }
)
} }
} }
} }
@@ -154,52 +242,70 @@ fun ManageCurrenciesScreen(
} }
@Composable @Composable
fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> Unit) { fun CurrencyRow(
Row( currency: DBCurrency,
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), mainCurrencyName: String,
verticalAlignment = Alignment.CenterVertically 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()
) { ) {
// Table-like layout
Row( Row(
modifier = Modifier.weight(1f), modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onEdit)
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Row(
text = "1 ", modifier = Modifier.weight(1f),
style = MaterialTheme.typography.body1, verticalAlignment = Alignment.CenterVertically
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) ) {
)
Text(
text = mainCurrencyName,
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 8.dp)
)
Column {
Text( Text(
text = currency.exchangeRate.toString(), text = "1 ",
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body2,
fontWeight = FontWeight.ExtraBold color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
) )
Text( Text(
text = currency.name ?: "", text = mainCurrencyName.ifEmpty { "$" },
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 8.dp)
)
Text(
text = formatAmount(currency.exchangeRate),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.ExtraBold,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
)
Text(
text = " ${currency.name ?: ""}",
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
) )
} }
}
IconButton(onClick = onDelete) {
IconButton(onClick = onDelete) { Icon(
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error) imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colors.error.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp)
)
}
} }
} }
Divider()
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@@ -209,6 +315,8 @@ fun CurrencyRowPreview() {
CurrencyRow( CurrencyRow(
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0), currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
mainCurrencyName = "EUR", mainCurrencyName = "EUR",
isEditing = false,
onEdit = {},
onDelete = {} onDelete = {}
) )
} }
@@ -230,7 +338,9 @@ fun ManageCurrenciesScreenPreview() {
onBack = {}, onBack = {},
onSaveMain = {}, onSaveMain = {},
onAdd = {}, onAdd = {},
onDelete = {} onDelete = {},
onEdit = {},
onCancelEdit = {}
) )
} }
} }
@@ -13,6 +13,8 @@ class ManageCurrenciesViewModel : ViewModel() {
var newCurrencyName by mutableStateOf("") var newCurrencyName by mutableStateOf("")
var newCurrencyRate by mutableStateOf("") var newCurrencyRate by mutableStateOf("")
var editingCurrencyId by mutableStateOf<Long?>(null)
var currencies by mutableStateOf<List<DBCurrency>>(emptyList()) var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
var dialogState by mutableStateOf<DialogState?>(null) var dialogState by mutableStateOf<DialogState?>(null)
@@ -1,20 +1,80 @@
package net.helcel.cowspent.android.helper package net.helcel.cowspent.android.helper
import net.helcel.cowspent.util.SupportUtil import java.text.NumberFormat
import java.util.Locale import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.round import kotlin.math.round
/**
* Formats amount using k/M notation (e.g., 1.5k for 1500).
*/
fun formatShortValue(value: Double): String { fun formatShortValue(value: Double): String {
val absValue = abs(value)
val sign = if (value < 0) "-" else ""
return when { return when {
value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "") absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M"
value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "") absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k"
else -> String.format(Locale.ROOT, "%.0f", value) else -> sign + formatAmount(absValue)
} }
} }
/**
* Formats balance with sign and locale-aware number formatting.
*/
fun formatBalance(balance: Double): String { fun formatBalance(balance: Double): String {
val rbalance = round(abs(balance) * 100.0) / 100.0 val rbalance = round(abs(balance) * 100.0) / 100.0
val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else "" val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else ""
return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(rbalance)}" return if (rbalance == 0.0) "" else "$balanceSign${formatAmount(rbalance)}"
}
/**
* Formats amount using system locale or specified locale.
*/
fun formatAmount(value: Double, locale: Locale = Locale.getDefault()): String {
val formatter = NumberFormat.getNumberInstance(locale)
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 0
return formatter.format(value)
}
/**
* Parses amount from string robustly.
* Tries common separators and cleans up non-numeric characters (except separators).
*/
fun parseAmount(input: String?): Double? {
if (input.isNullOrBlank()) return null
// Remove non-numeric characters except for delimiters and minus sign
val cleaned = input.replace(Regex("[^0-9,.-]"), "").trim()
if (cleaned.isEmpty()) return null
// If there's both a comma and a dot, we assume the last one is the decimal separator
val lastDot = cleaned.lastIndexOf('.')
val lastComma = cleaned.lastIndexOf(',')
return if (lastDot != -1 && lastComma != -1) {
if (lastDot > lastComma) {
// Dot is decimal, comma is thousands
cleaned.replace(",", "").toDoubleOrNull()
} else {
// Comma is decimal, dot is thousands
cleaned.replace(".", "").replace(",", ".").toDoubleOrNull()
}
} else if (lastComma != -1) {
// Only comma. Could be decimal or thousands.
if (cleaned.count { it == ',' } == 1) {
cleaned.replace(',', '.').toDoubleOrNull()
} else {
// Multiple commas -> thousands
cleaned.replace(",", "").toDoubleOrNull()
}
} else {
// Only dots or no separators
if (cleaned.count { it == '.' } > 1) {
cleaned.replace(".", "").toDoubleOrNull()
} else {
cleaned.toDoubleOrNull()
}
}
} }
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.formatAmount
import net.helcel.cowspent.android.helper.MemberAvatar import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
@@ -63,6 +63,7 @@ fun NewProjectScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.imePadding()
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxSize() .fillMaxSize()
@@ -2,6 +2,8 @@ package net.helcel.cowspent.android.project.edit
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -80,11 +82,14 @@ fun EditProjectScreen(
} }
} }
) { padding -> ) { padding ->
val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.imePadding()
.padding(16.dp) .padding(16.dp)
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState)
) { ) {
OutlinedTextField( OutlinedTextField(
value = viewModel.name, value = viewModel.name,
@@ -111,6 +111,7 @@ object ThemeUtils {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.navigationBarsPadding() .navigationBarsPadding()
.imePadding()
) { ) {
content() content()
} }