4 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
soraefir 5c74336c10 Fixes for currency, logo, render and more 2026-06-27 12:18:33 +02:00
20 changed files with 507 additions and 260 deletions
@@ -187,7 +187,12 @@ class EditBillActivity : AppCompatActivity() {
}
}
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 currencies = db.getCurrencies(bill.projectId)
withContext(Dispatchers.Main) {
@@ -209,6 +214,7 @@ class EditBillActivity : AppCompatActivity() {
calendar.timeInMillis = austrianBill.date.time
viewModel.timestamp = calendar.timeInMillis / 1000
viewModel.amount = austrianBill.amount.toString()
viewModel.updateSplits()
return@registerForActivityResult
} catch (_: ParseException) {
}
@@ -220,6 +226,7 @@ class EditBillActivity : AppCompatActivity() {
viewModel.timestamp = calendar.timeInMillis / 1000
}
viewModel.amount = croatianBill.amount.toString()
viewModel.updateSplits()
return@registerForActivityResult
} catch (_: ParseException) {
}
@@ -235,7 +242,8 @@ class EditBillActivity : AppCompatActivity() {
viewModel.repeat, bill.paymentMode, viewModel.categoryRemoteId,
viewModel.getFinalComment(), viewModel.paymentModeRemoteId
)
viewModel.timestamp = bill.timestamp
calendar.timeInMillis = System.currentTimeMillis()
viewModel.timestamp = calendar.timeInMillis / 1000
viewModel.isNewBill = true
showToast(this, "Duplicating bill...")
@@ -117,6 +117,7 @@ fun EditBillScreen(
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.fillMaxSize()
.verticalScroll(scrollState)
@@ -188,7 +189,9 @@ fun BillBasicInfoSection(
placeholder = { Text("0") },
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { viewModel.mainCurrencyName }
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
viewModel.mainCurrencyName.ifEmpty { "$" }
}
TextIconDisplay(
textIcon = TextIcon.Symbol(currencyToShow),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
@@ -198,7 +201,7 @@ fun BillBasicInfoSection(
IconButton(
enabled = canEdit,
onClick = {
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "Main" }
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" }
val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
}
@@ -207,9 +210,7 @@ fun BillBasicInfoSection(
items = options,
onItemSelected = { index ->
if (index == 0) {
viewModel.selectedCurrencyName = ""
viewModel.selectedCurrencyRate = 1.0
viewModel.updateSplits()
viewModel.resetCurrency()
} else {
viewModel.convertCurrency(viewModel.currencies[index - 1])
}
@@ -217,7 +218,7 @@ fun BillBasicInfoSection(
)
}
) {
Icon(Icons.Default.SwapHoriz, contentDescription = null)
Icon(Icons.Default.SwapHoriz, contentDescription = "Change Currency")
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
@@ -238,7 +239,7 @@ fun BillBasicInfoSection(
onClick = onDateClick,
modifier = Modifier.weight(1f),
enabled = canEdit,
leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }
leadingIcon = { Icon(Icons.Default.Event, contentDescription = "Select Date") }
)
Spacer(modifier = Modifier.width(8.dp))
ClickableOutlinedTextField(
@@ -246,7 +247,7 @@ fun BillBasicInfoSection(
onClick = onTimeClick,
modifier = Modifier.weight(1f),
enabled = canEdit,
leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = null) }
leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = "Select Time") }
)
}
}
@@ -269,12 +270,8 @@ fun PayerSection(
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
if (selectedPayer != null) {
UserAvatar(
name = selectedPayer.name,
r = selectedPayer.r,
g = selectedPayer.g,
b = selectedPayer.b,
disabled = !selectedPayer.isActivated,
MemberAvatar(
member = selectedPayer,
size = 24.dp
)
} else {
@@ -288,12 +285,8 @@ fun PayerSection(
viewModel.payerId = member.id
payerExpanded = false
}) {
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 24.dp
)
Spacer(modifier = Modifier.width(8.dp))
@@ -398,12 +391,8 @@ fun OwerSelectionSection(
enabled = canEdit,
onCheckedChange = { viewModel.toggleMember(member.id, it) }
)
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 32.dp
)
Spacer(modifier = Modifier.width(8.dp))
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
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.evalMath
@@ -41,13 +42,8 @@ class EditBillViewModel : ViewModel() {
val amountAsDouble: Double
get() {
val amountStr = amount.replace(',', '.')
return try {
if (amountStr.matches("[0-9.]+".toRegex())) {
amountStr.toDouble()
} else {
evalMath(amountStr)
}
return parseAmount(amount) ?: try {
evalMath(amount.replace(',', '.'))
} catch (_: Exception) {
0.0
}
@@ -131,8 +127,6 @@ class EditBillViewModel : ViewModel() {
}
fun convertCurrency(currency: DBCurrency) {
if (amountAsDouble == 0.0) return
selectedCurrencyName = currency.name ?: ""
selectedCurrencyRate = currency.exchangeRate
@@ -142,6 +136,12 @@ class EditBillViewModel : ViewModel() {
updateSplits()
}
fun resetCurrency() {
selectedCurrencyName = ""
selectedCurrencyRate = 1.0
updateSplits()
}
fun getFinalAmount(): Double {
return SupportUtil.round2(amountAsDouble / selectedCurrencyRate)
}
@@ -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) {
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 = ""
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,26 @@ 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
@Composable
@@ -26,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) {
@@ -75,78 +90,151 @@ fun ManageCurrenciesScreen(
Column(
modifier = Modifier
.padding(padding)
.padding(16.dp)
.imePadding()
.fillMaxSize()
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.02f))
) {
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)
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = onSaveMain, enabled = viewModel.mainCurrencyName.isNotEmpty()) {
Text(stringResource(R.string.save_or_discard_bill_dialog_save))
LaunchedEffect(viewModel.mainCurrencyName) {
if (viewModel.mainCurrencyName.isNotEmpty()) {
delay(500)
onSaveMain()
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
// Visual relationship indicator
Surface(
color = MaterialTheme.colors.primary.copy(alpha = 0.05f),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
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("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)
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))
// 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) {
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()
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(stringResource(R.string.simple_add))
}
Spacer(modifier = Modifier.height(24.dp))
LazyColumn(modifier = Modifier.weight(1f)) {
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
fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
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()
) {
// Table-like layout
Row(
modifier = Modifier.weight(1f),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onEdit)
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "1 ",
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 {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = currency.exchangeRate.toString(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.ExtraBold
text = "1 ",
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Text(
text = currency.name ?: "",
style = MaterialTheme.typography.caption,
text = mainCurrencyName.ifEmpty { "$" },
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,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error)
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colors.error.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp)
)
}
}
}
Divider()
}
@Preview(showBackground = true)
@@ -209,6 +315,8 @@ fun CurrencyRowPreview() {
CurrencyRow(
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
mainCurrencyName = "EUR",
isEditing = false,
onEdit = {},
onDelete = {}
)
}
@@ -230,7 +338,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)
@@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.android.helper.formatBalance
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
import net.helcel.cowspent.android.helper.TextIcon
@@ -279,13 +279,8 @@ fun DrawerItem(
verticalAlignment = Alignment.CenterVertically
) {
if (member != null) {
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
avatar = member.avatar,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 24.dp,
alpha = alpha
)
@@ -111,49 +111,54 @@ fun AlertDialog(
if (message != null) Spacer(Modifier.height(8.dp))
LazyColumn {
itemsIndexed(items) { index, item ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
}
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
val text = item.toString()
if (text.contains("|")) {
val parts = text.split("|")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val text = item.toString()
if (text.contains("|")) {
val parts = text.split("|")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = parts[0].trim(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = parts[1].trim(),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
} else {
Text(
text = parts[0].trim(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = parts[1].trim(),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
text = text,
style = MaterialTheme.typography.body1
)
}
} else {
Text(
text = text,
style = MaterialTheme.typography.body1
)
}
if (index < items.size - 1) {
Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
}
}
}
@@ -239,49 +244,54 @@ fun AlertDialogContent(
if (message != null) Spacer(Modifier.height(8.dp))
LazyColumn {
itemsIndexed(items) { index, item ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
val text = item.toString()
if (text.contains("|")) {
val parts = text.split("|")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val text = item.toString()
if (text.contains("|")) {
val parts = text.split("|")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = parts[0].trim(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = parts[1].trim(),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
} else {
Text(
text = parts[0].trim(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = parts[1].trim(),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
text = text,
style = MaterialTheme.typography.body1
)
}
} else {
Text(
text = text,
style = MaterialTheme.typography.body1
)
}
if (index < items.size - 1) {
Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
}
}
}
@@ -1,20 +1,80 @@
package net.helcel.cowspent.android.helper
import net.helcel.cowspent.util.SupportUtil
import java.text.NumberFormat
import java.util.Locale
import kotlin.math.abs
import kotlin.math.round
/**
* Formats amount using k/M notation (e.g., 1.5k for 1500).
*/
fun formatShortValue(value: Double): String {
val absValue = abs(value)
val sign = if (value < 0) "-" else ""
return when {
value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "")
value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "")
else -> String.format(Locale.ROOT, "%.0f", value)
absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M"
absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k"
else -> sign + formatAmount(absValue)
}
}
/**
* Formats balance with sign and locale-aware number formatting.
*/
fun formatBalance(balance: Double): String {
val rbalance = round(abs(balance) * 100.0) / 100.0
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()
}
}
}
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import android.graphics.BitmapFactory
import android.util.Base64
import net.helcel.cowspent.model.DBMember
@Composable
fun UserAvatar(
@@ -91,6 +92,26 @@ fun UserAvatar(
}
}
@Composable
fun MemberAvatar(
member: DBMember,
modifier: Modifier = Modifier,
size: Dp = 40.dp,
alpha: Float = 1f
) {
UserAvatar(
name = member.name,
modifier = modifier,
size = size,
r = member.r,
g = member.g,
b = member.b,
avatar = member.avatar,
disabled = !member.isActivated,
alpha = alpha
)
}
@Preview
@Composable
fun UserAvatarPreview() {
@@ -16,7 +16,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.formatAmount
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil
@@ -83,13 +84,8 @@ fun BillItemRow(bill: DBBill, payer: DBMember?, onClick: () -> Unit) {
) {
if (payer != null) {
Box {
UserAvatar(
name = payer.name,
r = payer.r,
g = payer.g,
b = payer.b,
avatar = payer.avatar,
disabled = !payer.isActivated,
MemberAvatar(
member = payer,
size = 40.dp
)
if (bill.repeat != null && bill.repeat != DBBill.NON_REPEATED) {
@@ -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,
@@ -30,7 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
import net.helcel.cowspent.model.DBMember
@@ -84,13 +84,8 @@ fun MemberManagementDialogContent(
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
avatar = member.avatar,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 40.dp
)
Spacer(modifier = Modifier.width(16.dp))
@@ -66,7 +66,7 @@ import androidx.core.graphics.toColorInt
import kotlinx.coroutines.launch
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.EditableExposedDropdownMenu
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
@@ -170,7 +170,7 @@ fun ProjectSankeyDiagram(
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
if (selectedMember != null) {
UserAvatar(name = selectedMember.name, r = selectedMember.r, g = selectedMember.g, b = selectedMember.b, disabled = !selectedMember.isActivated, size = 24.dp)
MemberAvatar(member = selectedMember, size = 24.dp)
} else Icon(Icons.Default.Group, contentDescription = null)
}
},
@@ -182,7 +182,7 @@ fun ProjectSankeyDiagram(
}
allMembers.forEach { member ->
DropdownMenuItem(onClick = { selectedMemberId = member.id; expanded = false }) {
UserAvatar(name = member.name, r = member.r, g = member.g, b = member.b, disabled = !member.isActivated, size = 24.dp)
MemberAvatar(member = member, size = 24.dp)
Spacer(modifier = Modifier.width(12.dp))
Text(member.name)
}
@@ -111,6 +111,7 @@ object ThemeUtils {
modifier = Modifier
.weight(1f)
.navigationBarsPadding()
.imePadding()
) {
content()
}
@@ -29,7 +29,6 @@
<path android:fillColor="#a57939" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
<path android:fillColor="#a57939" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
<path android:fillColor="#f4aa41" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
</group>
<group android:name="line">
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
@@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="72"
android:viewportHeight="72">
<group
android:scaleX="0.63461536"
android:scaleY="0.63461536"
android:translateX="13.153846"
android:translateY="13.153846">
<!-- Circle Outline -->
<path
android:strokeColor="#000"
android:strokeWidth="2.5"
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
<path
android:strokeColor="#000"
android:strokeWidth="2.5"
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
<!-- Cow Outlines -->
<group android:name="color">
<path android:fillColor="#000" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
<path android:fillColor="#000" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
<path android:fillColor="#000" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
<path android:fillColor="#000" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
</group>
</group>
</vector>
@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>