9 Commits
Author SHA1 Message Date
soraefir 5c74336c10 Fixes for currency, logo, render and more 2026-06-27 12:18:33 +02:00
soraefir d617c5c7be project creation and currency db 2026-06-14 21:34:58 +02:00
soraefir a9a303478c better currency handling 2026-06-14 21:33:08 +02:00
sora 110ab397b8 Update README.md 2026-06-09 19:19:24 +02:00
sora 3675e87651 Upload files to ".github/images" 2026-06-09 19:18:02 +02:00
sora 65799496f9 Update app/build.gradle 2026-06-09 08:51:43 +02:00
soraefir 2967dbd001 fix ui 2026-06-09 01:51:42 +02:00
sora 5c43bedc77 Update README.md 2026-05-30 00:00:04 +02:00
soraefir 5ee2041769 init
CI-Android APK / build (push) Failing after 4m27s
2026-05-29 23:19:55 +02:00
11 changed files with 123 additions and 331 deletions
@@ -187,12 +187,7 @@ class EditBillActivity : AppCompatActivity() {
} }
} }
calendar.timeInMillis = bill.timestamp * 1000 calendar.timeInMillis = bill.timestamp * 1000
val allMembers = db.getMembersOfProject(bill.projectId, null) val members = 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,7 +117,6 @@ 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)
@@ -189,9 +188,7 @@ fun BillBasicInfoSection(
placeholder = { Text("0") }, placeholder = { Text("0") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
leadingIcon = { leadingIcon = {
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { viewModel.mainCurrencyName }
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)
@@ -201,7 +198,7 @@ fun BillBasicInfoSection(
IconButton( IconButton(
enabled = canEdit, enabled = canEdit,
onClick = { onClick = {
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" } val mainLabel = viewModel.mainCurrencyName.ifEmpty { "Main" }
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,7 +10,6 @@ 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
@@ -42,8 +41,13 @@ class EditBillViewModel : ViewModel() {
val amountAsDouble: Double val amountAsDouble: Double
get() { get() {
return parseAmount(amount) ?: try { val amountStr = amount.replace(',', '.')
evalMath(amount.replace(',', '.')) return try {
if (amountStr.matches("[0-9.]+".toRegex())) {
amountStr.toDouble()
} else {
evalMath(amountStr)
}
} catch (_: Exception) { } catch (_: Exception) {
0.0 0.0
} }
@@ -67,10 +67,8 @@ class ManageCurrenciesActivity : AppCompatActivity() {
viewModel = viewModel, viewModel = viewModel,
onBack = { finish() }, onBack = { finish() },
onSaveMain = { saveMainCurrency() }, onSaveMain = { saveMainCurrency() },
onAdd = { addOrUpdateCurrency() }, onAdd = { addCurrency() },
onDelete = { deleteCurrency(it) }, onDelete = { deleteCurrency(it) }
onEdit = { startEditing(it) },
onCancelEdit = { cancelEditing() }
) )
} }
} }
@@ -107,42 +105,20 @@ class ManageCurrenciesActivity : AppCompatActivity() {
} }
} }
private fun addOrUpdateCurrency() { private fun addCurrency() {
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 } val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
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( val newCurrency = DBCurrency(
0, 0, selectedProjectID, 0, 0, selectedProjectID,
currencyName, exchangeRate, DBBill.STATE_ADDED viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
) )
lifecycleScope.launch {
withContext(Dispatchers.IO) {
db!!.addCurrencyAndSync(newCurrency) db!!.addCurrencyAndSync(newCurrency)
} }
}
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.newCurrencyName = ""
viewModel.newCurrencyRate = "" viewModel.newCurrencyRate = ""
updateCurrenciesList()
}
} }
private fun deleteCurrency(currency: DBCurrency) { private fun deleteCurrency(currency: DBCurrency) {
@@ -1,9 +1,6 @@
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
@@ -11,26 +8,16 @@ 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
@@ -39,9 +26,7 @@ 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) {
@@ -90,151 +75,78 @@ fun ManageCurrenciesScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.imePadding() .padding(16.dp)
.fillMaxSize() .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( OutlinedTextField(
value = viewModel.mainCurrencyName, value = viewModel.mainCurrencyName,
onValueChange = { viewModel.mainCurrencyName = it }, onValueChange = { viewModel.mainCurrencyName = it },
modifier = Modifier.fillMaxWidth(), label = { Text(stringResource(R.string.currency_edit_name)) },
placeholder = { Text("e.g. EUR", fontSize = 14.sp) }, modifier = Modifier.weight(1f)
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)) Spacer(modifier = Modifier.height(24.dp))
// Section 2: Exchange Rates Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
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 // Visual relationship indicator
Surface( Surface(
color = MaterialTheme.colors.surface, color = MaterialTheme.colors.primary.copy(alpha = 0.05f),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
elevation = 2.dp
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) { ) {
Text("1 ", style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)) Text("1 ", style = MaterialTheme.typography.body1)
Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontWeight = FontWeight.Bold) Text(viewModel.mainCurrencyName.ifEmpty { "Base" }, fontWeight = FontWeight.Bold)
Text( Text(" = ", style = MaterialTheme.typography.h6)
text = " = ", Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary)
style = MaterialTheme.typography.h6, Text(" ")
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, Text(viewModel.newCurrencyName.ifEmpty { "Currency" }, fontWeight = FontWeight.Bold)
modifier = Modifier.padding(horizontal = 4.dp) }
) }
OutlinedTextField( Row(verticalAlignment = Alignment.CenterVertically) {
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( OutlinedTextField(
value = viewModel.newCurrencyName, value = viewModel.newCurrencyName,
onValueChange = { viewModel.newCurrencyName = it }, onValueChange = { viewModel.newCurrencyName = it },
modifier = Modifier.weight(1f).height(52.dp), label = { Text(stringResource(R.string.currency_edit_name)) },
placeholder = { Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontSize = 12.sp) }, modifier = Modifier.weight(1f),
singleLine = true, placeholder = { Text("e.g. USD") }
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)
) )
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") }
) )
}
Row(modifier = Modifier.padding(start = 4.dp)) { Spacer(modifier = Modifier.height(8.dp))
IconButton( Button(
onClick = onAdd, onClick = onAdd,
modifier = Modifier.fillMaxWidth(),
enabled = viewModel.isAddEnabled() enabled = viewModel.isAddEnabled()
) { ) {
Icon( Text(stringResource(R.string.simple_add))
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) { Spacer(modifier = Modifier.height(24.dp))
IconButton(onClick = onCancelEdit) {
Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.error)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp)) LazyColumn(modifier = Modifier.weight(1f)) {
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 -> items(viewModel.currencies) { currency ->
CurrencyRow( CurrencyRow(currency, viewModel.mainCurrencyName, onDelete = { onDelete(currency) })
currency = currency,
mainCurrencyName = viewModel.mainCurrencyName,
isEditing = viewModel.editingCurrencyId == currency.id,
onEdit = { onEdit(currency) },
onDelete = { onDelete(currency) }
)
} }
} }
} }
@@ -242,70 +154,52 @@ fun ManageCurrenciesScreen(
} }
@Composable @Composable
fun CurrencyRow( fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> Unit) {
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( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
.fillMaxWidth()
.clickable(onClick = onEdit)
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Table-like layout
Row( Row(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "1 ", text = "1 ",
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
) )
Text( Text(
text = mainCurrencyName.ifEmpty { "$" }, text = mainCurrencyName,
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
text = " = ", text = " = ",
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp)
) )
Column {
Text( Text(
text = formatAmount(currency.exchangeRate), text = currency.exchangeRate.toString(),
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
fontWeight = FontWeight.ExtraBold, fontWeight = FontWeight.ExtraBold
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
) )
Text( Text(
text = " ${currency.name ?: ""}", text = currency.name ?: "",
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.caption,
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)
@@ -315,8 +209,6 @@ 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 = {}
) )
} }
@@ -338,9 +230,7 @@ fun ManageCurrenciesScreenPreview() {
onBack = {}, onBack = {},
onSaveMain = {}, onSaveMain = {},
onAdd = {}, onAdd = {},
onDelete = {}, onDelete = {}
onEdit = {},
onCancelEdit = {}
) )
} }
} }
@@ -13,8 +13,6 @@ 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,80 +1,20 @@
package net.helcel.cowspent.android.helper package net.helcel.cowspent.android.helper
import java.text.NumberFormat import net.helcel.cowspent.util.SupportUtil
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 {
absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M" value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "")
absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k" value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "")
else -> sign + formatAmount(absValue) else -> String.format(Locale.ROOT, "%.0f", value)
} }
} }
/**
* 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${formatAmount(rbalance)}" return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(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,7 +16,6 @@ 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,7 +63,6 @@ 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,8 +2,6 @@ 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
@@ -82,14 +80,11 @@ 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,7 +111,6 @@ object ThemeUtils {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.navigationBarsPadding() .navigationBarsPadding()
.imePadding()
) { ) {
content() content()
} }