Compare commits
9
Commits
1.1a
..
5c74336c10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c74336c10
|
||
|
|
d617c5c7be
|
||
|
|
a9a303478c
|
||
|
|
110ab397b8 | ||
|
|
3675e87651 | ||
|
|
65799496f9 | ||
|
|
2967dbd001
|
||
|
|
5c43bedc77 | ||
|
|
5ee2041769
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-34
@@ -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,44 +105,22 @@ 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 newCurrency = DBCurrency(
|
||||||
val editingId = viewModel.editingCurrencyId
|
0, 0, selectedProjectID,
|
||||||
|
viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
|
||||||
|
)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (editingId != null) {
|
db!!.addCurrencyAndSync(newCurrency)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cancelEditing()
|
viewModel.newCurrencyName = ""
|
||||||
|
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) {
|
||||||
|
|||||||
+96
-206
@@ -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) {
|
Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6)
|
||||||
if (viewModel.mainCurrencyName.isNotEmpty()) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
delay(500)
|
OutlinedTextField(
|
||||||
onSaveMain()
|
value = viewModel.mainCurrencyName,
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Spacer(modifier = Modifier.height(24.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()
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = viewModel.mainCurrencyName,
|
|
||||||
onValueChange = { viewModel.mainCurrencyName = it },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
placeholder = { Text("e.g. EUR", fontSize = 14.sp) },
|
|
||||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
|
||||||
focusedBorderColor = Color.Transparent,
|
|
||||||
unfocusedBorderColor = Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
|
||||||
|
|
||||||
// Section 2: Exchange Rates
|
// Visual relationship indicator
|
||||||
val isEditing = viewModel.editingCurrencyId != null
|
Surface(
|
||||||
Text(
|
color = MaterialTheme.colors.primary.copy(alpha = 0.05f),
|
||||||
text = (if (isEditing) "Edit exchange rate" else "Add exchange rate").uppercase(),
|
shape = RoundedCornerShape(8.dp),
|
||||||
style = MaterialTheme.typography.overline,
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
|
||||||
contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text("1 ", style = MaterialTheme.typography.body1)
|
||||||
|
Text(viewModel.mainCurrencyName.ifEmpty { "Base" }, 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") }
|
||||||
|
)
|
||||||
|
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(
|
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,
|
Row(
|
||||||
mainCurrencyName: String,
|
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||||
isEditing: Boolean,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
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
|
modifier = Modifier.weight(1f),
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onEdit)
|
|
||||||
.padding(16.dp, 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
text = "1 ",
|
||||||
verticalAlignment = Alignment.CenterVertically
|
style = MaterialTheme.typography.body1,
|
||||||
) {
|
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 = "1 ",
|
text = currency.exchangeRate.toString(),
|
||||||
style = MaterialTheme.typography.body2,
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = mainCurrencyName.ifEmpty { "$" },
|
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.ExtraBold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = " = ",
|
text = currency.name ?: "",
|
||||||
style = MaterialTheme.typography.h6,
|
style = MaterialTheme.typography.caption,
|
||||||
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)
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user