Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
84432fe68d
|
|||
|
7e00b14d27
|
|||
|
ec4d60897a
|
|||
|
5c74336c10
|
|||
|
d617c5c7be
|
|||
|
a9a303478c
|
|||
| 110ab397b8 | |||
| 3675e87651 |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -4,6 +4,7 @@
|
|||||||
<img width="100px" src="./metadata/en-US/images/icon.webp" alt="Logo">
|
<img width="100px" src="./metadata/en-US/images/icon.webp" alt="Logo">
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
Shared budget manager able to sync with [Nextcloud Cospend](https://github.com/julien-nc/cospend-nc).
|
Shared budget manager able to sync with [Nextcloud Cospend](https://github.com/julien-nc/cospend-nc).
|
||||||
|
|
||||||
This work is based upon [MoneyBuster](https://gitlab.com/eneiluj/moneybuster).
|
This work is based upon [MoneyBuster](https://gitlab.com/eneiluj/moneybuster).
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
|||||||
import net.helcel.cowspent.theme.ThemeUtils
|
import net.helcel.cowspent.theme.ThemeUtils
|
||||||
import net.helcel.cowspent.util.BillParser
|
import net.helcel.cowspent.util.BillParser
|
||||||
import net.helcel.cowspent.util.CategoryUtils
|
import net.helcel.cowspent.util.CategoryUtils
|
||||||
|
import net.helcel.cowspent.util.SupportUtil
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@@ -107,7 +108,8 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java)
|
val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java)
|
||||||
scanQRCodeLauncher.launch(createIntent)
|
scanQRCodeLauncher.launch(createIntent)
|
||||||
},
|
},
|
||||||
onDelete = if (bill.id > 0) { { deleteBillAsked() } } else null,
|
onDuplicate = if (!viewModel.isNewBill) { { duplicateCurrentBill() } } else null,
|
||||||
|
onDelete = if (!viewModel.isNewBill) { { deleteBillAsked() } } else null,
|
||||||
accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN
|
accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -185,12 +187,18 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
calendar.timeInMillis = bill.timestamp * 1000
|
calendar.timeInMillis = bill.timestamp * 1000
|
||||||
val members = db.getMembersOfProject(bill.projectId, null)
|
val allMembers = db.getMembersOfProject(bill.projectId, null)
|
||||||
|
val billOwerIds = bill.billOwersIds
|
||||||
|
val members = allMembers.filter { m ->
|
||||||
|
m.isActivated || m.id == bill.payerId || billOwerIds.contains(m.id)
|
||||||
|
}
|
||||||
|
|
||||||
val project = db.getProject(bill.projectId)
|
val project = db.getProject(bill.projectId)
|
||||||
val currencies = db.getCurrencies(bill.projectId)
|
val currencies = db.getCurrencies(bill.projectId)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
viewModel.currencies = currencies
|
viewModel.currencies = currencies
|
||||||
viewModel.mainCurrencyName = project?.currencyName ?: ""
|
viewModel.mainCurrencyName = project?.currencyName ?: ""
|
||||||
|
viewModel.isNewBill = (bill.id == 0L)
|
||||||
viewModel.initFromBill(bill, members, customSplits)
|
viewModel.initFromBill(bill, members, customSplits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,6 +214,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
calendar.timeInMillis = austrianBill.date.time
|
calendar.timeInMillis = austrianBill.date.time
|
||||||
viewModel.timestamp = calendar.timeInMillis / 1000
|
viewModel.timestamp = calendar.timeInMillis / 1000
|
||||||
viewModel.amount = austrianBill.amount.toString()
|
viewModel.amount = austrianBill.amount.toString()
|
||||||
|
viewModel.updateSplits()
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
} catch (_: ParseException) {
|
} catch (_: ParseException) {
|
||||||
}
|
}
|
||||||
@@ -217,6 +226,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
viewModel.timestamp = calendar.timeInMillis / 1000
|
viewModel.timestamp = calendar.timeInMillis / 1000
|
||||||
}
|
}
|
||||||
viewModel.amount = croatianBill.amount.toString()
|
viewModel.amount = croatianBill.amount.toString()
|
||||||
|
viewModel.updateSplits()
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
} catch (_: ParseException) {
|
} catch (_: ParseException) {
|
||||||
}
|
}
|
||||||
@@ -225,6 +235,20 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun duplicateCurrentBill() {
|
||||||
|
bill = DBBill(
|
||||||
|
0, 0, bill.projectId, viewModel.payerId, viewModel.amountAsDouble,
|
||||||
|
System.currentTimeMillis() / 1000, viewModel.what, DBBill.STATE_ADDED,
|
||||||
|
viewModel.repeat, bill.paymentMode, viewModel.categoryRemoteId,
|
||||||
|
viewModel.getFinalComment(), viewModel.paymentModeRemoteId
|
||||||
|
)
|
||||||
|
calendar.timeInMillis = System.currentTimeMillis()
|
||||||
|
viewModel.timestamp = calendar.timeInMillis / 1000
|
||||||
|
viewModel.isNewBill = true
|
||||||
|
|
||||||
|
showToast(this, "Duplicating bill...")
|
||||||
|
}
|
||||||
|
|
||||||
private fun onBack() {
|
private fun onBack() {
|
||||||
if (!valuesHaveChanged()) {
|
if (!valuesHaveChanged()) {
|
||||||
finish()
|
finish()
|
||||||
@@ -298,9 +322,9 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
return !(bill.what == viewModel.what &&
|
return !(bill.what == viewModel.what &&
|
||||||
bill.timestamp == viewModel.timestamp &&
|
bill.timestamp == viewModel.timestamp &&
|
||||||
bill.amount == viewModel.amountAsDouble &&
|
bill.amount == viewModel.getFinalAmount() &&
|
||||||
bill.payerId == viewModel.payerId &&
|
bill.payerId == viewModel.payerId &&
|
||||||
bill.comment == viewModel.comment &&
|
bill.comment == viewModel.getFinalComment() &&
|
||||||
bill.repeat == viewModel.repeat &&
|
bill.repeat == viewModel.repeat &&
|
||||||
bill.categoryRemoteId == viewModel.categoryRemoteId &&
|
bill.categoryRemoteId == viewModel.categoryRemoteId &&
|
||||||
bill.paymentModeRemoteId == viewModel.paymentModeRemoteId &&
|
bill.paymentModeRemoteId == viewModel.paymentModeRemoteId &&
|
||||||
@@ -312,13 +336,17 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
val isCustomSplit = viewModel.isCustomSplit
|
val isCustomSplit = viewModel.isCustomSplit
|
||||||
|
|
||||||
if (isCustomSplit) {
|
if (isCustomSplit) {
|
||||||
val splits = viewModel.owersCustomSplit.filter { (id, amount) ->
|
val splits: Map<Long, Double> = viewModel.owersCustomSplit.filter { (id, amountStr) ->
|
||||||
viewModel.owersSelection[id] == true && (amount.replace(',', '.').toDoubleOrNull()
|
viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull()
|
||||||
?: 0.0) > 0
|
?: 0.0) > 0
|
||||||
}.mapValues { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 }
|
}.mapValues {
|
||||||
|
val uiAmount = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0
|
||||||
|
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
|
||||||
|
}
|
||||||
|
|
||||||
if (splits.isEmpty()) return@withContext 0L
|
if (splits.isEmpty()) return@withContext 0L
|
||||||
|
|
||||||
|
val finalComment = viewModel.getFinalComment()
|
||||||
val splitEntries = splits.entries.toList()
|
val splitEntries = splits.entries.toList()
|
||||||
|
|
||||||
// Pool of existing bills in this group that we can potentially reuse
|
// Pool of existing bills in this group that we can potentially reuse
|
||||||
@@ -360,7 +388,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
existingBill.paymentMode,
|
existingBill.paymentMode,
|
||||||
viewModel.paymentModeRemoteId,
|
viewModel.paymentModeRemoteId,
|
||||||
viewModel.categoryRemoteId,
|
viewModel.categoryRemoteId,
|
||||||
viewModel.comment
|
finalComment
|
||||||
)
|
)
|
||||||
if (firstSavedId == 0L) firstSavedId = billToUseId
|
if (firstSavedId == 0L) firstSavedId = billToUseId
|
||||||
} else {
|
} else {
|
||||||
@@ -368,7 +396,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
val newBill = DBBill(
|
val newBill = DBBill(
|
||||||
0, 0, bill.projectId, viewModel.payerId, amount,
|
0, 0, bill.projectId, viewModel.payerId, amount,
|
||||||
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
|
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
|
||||||
bill.paymentMode, viewModel.categoryRemoteId, viewModel.comment, viewModel.paymentModeRemoteId
|
bill.paymentMode, viewModel.categoryRemoteId, finalComment, viewModel.paymentModeRemoteId
|
||||||
)
|
)
|
||||||
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
|
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
|
||||||
val newId = db.addBill(newBill)
|
val newId = db.addBill(newBill)
|
||||||
@@ -398,7 +426,8 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
return@withContext firstSavedId
|
return@withContext firstSavedId
|
||||||
} else {
|
} else {
|
||||||
val newAmount = viewModel.amountAsDouble
|
val newAmount = viewModel.getFinalAmount()
|
||||||
|
val finalComment = viewModel.getFinalComment()
|
||||||
val newOwersIds = viewModel.getOwersIds()
|
val newOwersIds = viewModel.getOwersIds()
|
||||||
|
|
||||||
if (bill.id != 0L) {
|
if (bill.id != 0L) {
|
||||||
@@ -414,7 +443,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
bill.paymentMode,
|
bill.paymentMode,
|
||||||
viewModel.paymentModeRemoteId,
|
viewModel.paymentModeRemoteId,
|
||||||
viewModel.categoryRemoteId,
|
viewModel.categoryRemoteId,
|
||||||
viewModel.comment
|
finalComment
|
||||||
)
|
)
|
||||||
if (groupedBillIds != null) {
|
if (groupedBillIds != null) {
|
||||||
for (id in groupedBillIds) {
|
for (id in groupedBillIds) {
|
||||||
@@ -431,7 +460,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
val newBill = DBBill(
|
val newBill = DBBill(
|
||||||
0, 0, bill.projectId, viewModel.payerId, newAmount,
|
0, 0, bill.projectId, viewModel.payerId, newAmount,
|
||||||
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
|
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
|
||||||
bill.paymentMode, viewModel.categoryRemoteId, viewModel.comment, viewModel.paymentModeRemoteId
|
bill.paymentMode, viewModel.categoryRemoteId, finalComment, viewModel.paymentModeRemoteId
|
||||||
)
|
)
|
||||||
newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) }
|
newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) }
|
||||||
val newBillId = db.addBill(newBill)
|
val newBillId = db.addBill(newBill)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ fun EditBillScreen(
|
|||||||
onDateClick: () -> Unit,
|
onDateClick: () -> Unit,
|
||||||
onTimeClick: () -> Unit,
|
onTimeClick: () -> Unit,
|
||||||
onScan: () -> Unit,
|
onScan: () -> Unit,
|
||||||
|
onDuplicate: (() -> Unit)? = null,
|
||||||
onDelete: (() -> Unit)? = null,
|
onDelete: (() -> Unit)? = null,
|
||||||
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN
|
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN
|
||||||
) {
|
) {
|
||||||
@@ -59,7 +60,7 @@ fun EditBillScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(if (viewModel.what.isEmpty()) R.string.simple_new_bill else R.string.simple_edit_bill)) },
|
title = { Text(stringResource(if (viewModel.isNewBill) R.string.simple_new_bill else R.string.simple_edit_bill)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||||
@@ -70,6 +71,11 @@ fun EditBillScreen(
|
|||||||
IconButton(onClick = onScan) {
|
IconButton(onClick = onScan) {
|
||||||
Icon(Icons.Default.QrCodeScanner, contentDescription = null)
|
Icon(Icons.Default.QrCodeScanner, contentDescription = null)
|
||||||
}
|
}
|
||||||
|
if (onDuplicate != null) {
|
||||||
|
IconButton(onClick = onDuplicate) {
|
||||||
|
Icon(Icons.Default.ContentCopy, contentDescription = "Duplicate")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (onDelete != null) {
|
if (onDelete != null) {
|
||||||
IconButton(onClick = onDelete) {
|
IconButton(onClick = onDelete) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete))
|
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete))
|
||||||
@@ -111,6 +117,7 @@ fun EditBillScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.imePadding()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
@@ -160,7 +167,6 @@ fun BillBasicInfoSection(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val currencyDialogTitle =
|
val currencyDialogTitle =
|
||||||
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
|
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
|
||||||
val noCurrencyError = stringResource(R.string.no_currency_error)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.what,
|
value = viewModel.what,
|
||||||
@@ -182,25 +188,37 @@ fun BillBasicInfoSection(
|
|||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
placeholder = { Text("0") },
|
placeholder = { Text("0") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
leadingIcon = { Icon(Icons.Default.AttachMoney, contentDescription = null) },
|
leadingIcon = {
|
||||||
|
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
|
||||||
|
viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||||
|
}
|
||||||
|
TextIconDisplay(
|
||||||
|
textIcon = TextIcon.Symbol(currencyToShow),
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (viewModel.currencies.isNotEmpty()) {
|
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||||
viewModel.showDialog(
|
val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
|
||||||
title = currencyDialogTitle,
|
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
|
||||||
items = viewModel.currencies.map { "${it.name} (${it.exchangeRate})" },
|
|
||||||
onItemSelected = { index ->
|
|
||||||
viewModel.convertCurrency(viewModel.currencies[index])
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showToast(context, noCurrencyError)
|
|
||||||
}
|
}
|
||||||
|
viewModel.showDialog(
|
||||||
|
title = currencyDialogTitle,
|
||||||
|
items = options,
|
||||||
|
onItemSelected = { index ->
|
||||||
|
if (index == 0) {
|
||||||
|
viewModel.resetCurrency()
|
||||||
|
} else {
|
||||||
|
viewModel.convertCurrency(viewModel.currencies[index - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.CurrencyExchange, contentDescription = null)
|
Icon(Icons.Default.SwapHoriz, contentDescription = "Change Currency")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
@@ -221,7 +239,7 @@ fun BillBasicInfoSection(
|
|||||||
onClick = onDateClick,
|
onClick = onDateClick,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }
|
leadingIcon = { Icon(Icons.Default.Event, contentDescription = "Select Date") }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
ClickableOutlinedTextField(
|
ClickableOutlinedTextField(
|
||||||
@@ -229,7 +247,7 @@ fun BillBasicInfoSection(
|
|||||||
onClick = onTimeClick,
|
onClick = onTimeClick,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = null) }
|
leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = "Select Time") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,12 +270,8 @@ fun PayerSection(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||||
if (selectedPayer != null) {
|
if (selectedPayer != null) {
|
||||||
UserAvatar(
|
MemberAvatar(
|
||||||
name = selectedPayer.name,
|
member = selectedPayer,
|
||||||
r = selectedPayer.r,
|
|
||||||
g = selectedPayer.g,
|
|
||||||
b = selectedPayer.b,
|
|
||||||
disabled = !selectedPayer.isActivated,
|
|
||||||
size = 24.dp
|
size = 24.dp
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -271,12 +285,8 @@ fun PayerSection(
|
|||||||
viewModel.payerId = member.id
|
viewModel.payerId = member.id
|
||||||
payerExpanded = false
|
payerExpanded = false
|
||||||
}) {
|
}) {
|
||||||
UserAvatar(
|
MemberAvatar(
|
||||||
name = member.name,
|
member = member,
|
||||||
r = member.r,
|
|
||||||
g = member.g,
|
|
||||||
b = member.b,
|
|
||||||
disabled = !member.isActivated,
|
|
||||||
size = 24.dp
|
size = 24.dp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
@@ -381,12 +391,8 @@ fun OwerSelectionSection(
|
|||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
onCheckedChange = { viewModel.toggleMember(member.id, it) }
|
onCheckedChange = { viewModel.toggleMember(member.id, it) }
|
||||||
)
|
)
|
||||||
UserAvatar(
|
MemberAvatar(
|
||||||
name = member.name,
|
member = member,
|
||||||
r = member.r,
|
|
||||||
g = member.g,
|
|
||||||
b = member.b,
|
|
||||||
disabled = !member.isActivated,
|
|
||||||
size = 32.dp
|
size = 32.dp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
@@ -587,6 +593,7 @@ fun EditBillScreenPreview() {
|
|||||||
viewModel = EditBillViewModel().apply {
|
viewModel = EditBillViewModel().apply {
|
||||||
what = "Pizza"
|
what = "Pizza"
|
||||||
amount = "12.50"
|
amount = "12.50"
|
||||||
|
mainCurrencyName = "EUR"
|
||||||
members = listOf(
|
members = listOf(
|
||||||
DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null),
|
DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null),
|
||||||
DBMember(2, 0, 0, "Bob", true, 1.0, 0, null, null, null, null, null)
|
DBMember(2, 0, 0, "Bob", true, 1.0, 0, null, null, null, null, null)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import net.helcel.cowspent.model.DBBill
|
import net.helcel.cowspent.model.DBBill
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
import net.helcel.cowspent.android.helper.DialogState
|
import net.helcel.cowspent.android.helper.DialogState
|
||||||
|
import net.helcel.cowspent.android.helper.parseAmount
|
||||||
import net.helcel.cowspent.util.SupportUtil
|
import net.helcel.cowspent.util.SupportUtil
|
||||||
import net.helcel.cowspent.util.evalMath
|
import net.helcel.cowspent.util.evalMath
|
||||||
|
|
||||||
@@ -25,9 +26,12 @@ class EditBillViewModel : ViewModel() {
|
|||||||
var repeat by mutableStateOf(DBBill.NON_REPEATED)
|
var repeat by mutableStateOf(DBBill.NON_REPEATED)
|
||||||
var paymentModeRemoteId by mutableIntStateOf(0)
|
var paymentModeRemoteId by mutableIntStateOf(0)
|
||||||
var categoryRemoteId by mutableIntStateOf(0)
|
var categoryRemoteId by mutableIntStateOf(0)
|
||||||
|
var isNewBill by mutableStateOf(false)
|
||||||
|
|
||||||
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
||||||
var mainCurrencyName by mutableStateOf("")
|
var mainCurrencyName by mutableStateOf("")
|
||||||
|
var selectedCurrencyName by mutableStateOf("")
|
||||||
|
var selectedCurrencyRate by mutableStateOf(1.0)
|
||||||
var members by mutableStateOf<List<DBMember>>(emptyList())
|
var members by mutableStateOf<List<DBMember>>(emptyList())
|
||||||
|
|
||||||
var owersSelection = mutableStateMapOf<Long, Boolean>()
|
var owersSelection = mutableStateMapOf<Long, Boolean>()
|
||||||
@@ -38,13 +42,8 @@ class EditBillViewModel : ViewModel() {
|
|||||||
|
|
||||||
val amountAsDouble: Double
|
val amountAsDouble: Double
|
||||||
get() {
|
get() {
|
||||||
val amountStr = amount.replace(',', '.')
|
return parseAmount(amount) ?: try {
|
||||||
return try {
|
evalMath(amount.replace(',', '.'))
|
||||||
if (amountStr.matches("[0-9.]+".toRegex())) {
|
|
||||||
amountStr.toDouble()
|
|
||||||
} else {
|
|
||||||
evalMath(amountStr)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
@@ -128,46 +127,81 @@ class EditBillViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun convertCurrency(currency: DBCurrency) {
|
fun convertCurrency(currency: DBCurrency) {
|
||||||
val originalAmountStr = amount
|
selectedCurrencyName = currency.name ?: ""
|
||||||
val originalAmount = amountAsDouble
|
selectedCurrencyRate = currency.exchangeRate
|
||||||
if (originalAmount == 0.0) return
|
|
||||||
|
// We don't change 'amount' here anymore as per request.
|
||||||
val newAmount = originalAmount / currency.exchangeRate
|
// It stays as the original amount.
|
||||||
amount = SupportUtil.round2(newAmount).toString()
|
|
||||||
|
|
||||||
val currencyLabel = currency.name ?: ""
|
|
||||||
val conversionNote = "($originalAmountStr $currencyLabel)"
|
|
||||||
if (!comment.contains(conversionNote)) {
|
|
||||||
if (comment.isNotEmpty() && !comment.endsWith(" ")) {
|
|
||||||
comment += " "
|
|
||||||
}
|
|
||||||
comment += conversionNote
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCustomSplit) {
|
|
||||||
owersCustomSplit.keys.toList().forEach { id ->
|
|
||||||
val value = owersCustomSplit[id] ?: ""
|
|
||||||
val partAmount = value.replace(',', '.').toDoubleOrNull() ?: 0.0
|
|
||||||
if (partAmount != 0.0) {
|
|
||||||
val newPartAmount = partAmount / currency.exchangeRate
|
|
||||||
owersCustomSplit[id] = SupportUtil.round2(newPartAmount).toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSplits()
|
updateSplits()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetCurrency() {
|
||||||
|
selectedCurrencyName = ""
|
||||||
|
selectedCurrencyRate = 1.0
|
||||||
|
updateSplits()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFinalAmount(): Double {
|
||||||
|
return SupportUtil.round2(amountAsDouble / selectedCurrencyRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFinalComment(): String {
|
||||||
|
var baseComment = comment
|
||||||
|
val regex = "\\s*#[^:]+:[\\d.,]+@[\\d.,]+".toRegex()
|
||||||
|
baseComment = baseComment.replace(regex, "").trim()
|
||||||
|
|
||||||
|
return if (selectedCurrencyName.isNotEmpty() && selectedCurrencyRate != 1.0) {
|
||||||
|
val metadata = "#$selectedCurrencyName:$amount@$selectedCurrencyRate"
|
||||||
|
if (baseComment.isEmpty()) metadata else "$baseComment $metadata"
|
||||||
|
} else {
|
||||||
|
baseComment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun initFromBill(bill: DBBill, members: List<DBMember>, customSplits: Map<Long, Double>? = null) {
|
fun initFromBill(bill: DBBill, members: List<DBMember>, customSplits: Map<Long, Double>? = null) {
|
||||||
this.members = members
|
this.members = members
|
||||||
what = bill.what
|
what = bill.what
|
||||||
amount = if (bill.amount == 0.0) "" else bill.amount.toString()
|
|
||||||
comment = bill.comment ?: ""
|
|
||||||
timestamp = bill.timestamp
|
timestamp = bill.timestamp
|
||||||
payerId = bill.payerId
|
payerId = bill.payerId
|
||||||
repeat = bill.repeat ?: DBBill.NON_REPEATED
|
repeat = bill.repeat ?: DBBill.NON_REPEATED
|
||||||
paymentModeRemoteId = bill.paymentModeRemoteId
|
paymentModeRemoteId = bill.paymentModeRemoteId
|
||||||
categoryRemoteId = bill.categoryRemoteId
|
categoryRemoteId = bill.categoryRemoteId
|
||||||
|
|
||||||
|
val rawComment = bill.comment ?: ""
|
||||||
|
|
||||||
|
// Try to parse existing conversion metadata #CURR:ORIG@RATE
|
||||||
|
// Using [^:]+ for currency name to support symbols and names with spaces
|
||||||
|
val regex = "#([^:]+):([\\d.,]+)@([\\d.,]+)".toRegex()
|
||||||
|
val match = regex.find(rawComment)
|
||||||
|
|
||||||
|
if (match != null) {
|
||||||
|
val (currName, origAmount, rate) = match.destructured
|
||||||
|
selectedCurrencyName = currName
|
||||||
|
selectedCurrencyRate = rate.replace(',', '.').toDoubleOrNull() ?: 1.0
|
||||||
|
amount = origAmount
|
||||||
|
comment = rawComment.replace(match.value, "").trim()
|
||||||
|
|
||||||
|
// Check if latest rate is different
|
||||||
|
val latestCurrency = currencies.find { it.name == selectedCurrencyName }
|
||||||
|
if (latestCurrency != null && latestCurrency.exchangeRate != selectedCurrencyRate) {
|
||||||
|
showDialog(
|
||||||
|
title = "Update Exchange Rate?",
|
||||||
|
message = "The exchange rate for $selectedCurrencyName has changed from $selectedCurrencyRate to ${latestCurrency.exchangeRate}. Do you want to update the conversion for the saved total?",
|
||||||
|
positiveText = "Update Rate",
|
||||||
|
negativeText = "Keep Old",
|
||||||
|
onConfirm = {
|
||||||
|
selectedCurrencyRate = latestCurrency.exchangeRate
|
||||||
|
updateSplits()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedCurrencyName = ""
|
||||||
|
selectedCurrencyRate = 1.0
|
||||||
|
amount = if (bill.amount == 0.0) "" else bill.amount.toString()
|
||||||
|
comment = rawComment
|
||||||
|
}
|
||||||
|
|
||||||
owersSelection.clear()
|
owersSelection.clear()
|
||||||
owersCustomSplit.clear()
|
owersCustomSplit.clear()
|
||||||
@@ -178,13 +212,23 @@ class EditBillViewModel : ViewModel() {
|
|||||||
val selected = customSplits.containsKey(member.id)
|
val selected = customSplits.containsKey(member.id)
|
||||||
owersSelection[member.id] = selected
|
owersSelection[member.id] = selected
|
||||||
if (selected) {
|
if (selected) {
|
||||||
owersCustomSplit[member.id] = SupportUtil.round2(customSplits[member.id]!!).toString()
|
// If we have metadata, the custom splits from DB are also converted.
|
||||||
|
// We should show them as "Original" if possible?
|
||||||
|
// Actually, if we use metadata, we should probably store original splits too,
|
||||||
|
// but for now let's just reverse the rate for display.
|
||||||
|
val dbPart = customSplits[member.id]!!
|
||||||
|
val uiPart = if (selectedCurrencyRate != 1.0) dbPart * selectedCurrencyRate else dbPart
|
||||||
|
owersCustomSplit[member.id] = SupportUtil.round2(uiPart).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Even split logic
|
||||||
val billOwerIds = bill.billOwersIds
|
val billOwerIds = bill.billOwersIds
|
||||||
val selectedCount = billOwerIds.size
|
val selectedCount = billOwerIds.size
|
||||||
val evenSplit = if (selectedCount > 0) bill.amount / selectedCount else 0.0
|
|
||||||
|
// Use UI amount for even split calculation
|
||||||
|
val uiAmount = amountAsDouble
|
||||||
|
val evenSplit = if (selectedCount > 0) uiAmount / selectedCount else 0.0
|
||||||
val evenSplitStr = if (evenSplit == 0.0) "" else SupportUtil.round2(evenSplit).toString()
|
val evenSplitStr = if (evenSplit == 0.0) "" else SupportUtil.round2(evenSplit).toString()
|
||||||
|
|
||||||
for (member in members) {
|
for (member in members) {
|
||||||
|
|||||||
+46
-15
@@ -15,6 +15,7 @@ import net.helcel.cowspent.R
|
|||||||
import net.helcel.cowspent.android.helper.showToast
|
import net.helcel.cowspent.android.helper.showToast
|
||||||
import net.helcel.cowspent.model.DBBill
|
import net.helcel.cowspent.model.DBBill
|
||||||
import net.helcel.cowspent.model.DBCurrency
|
import net.helcel.cowspent.model.DBCurrency
|
||||||
|
import net.helcel.cowspent.model.ProjectType
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
import net.helcel.cowspent.theme.ThemeUtils
|
import net.helcel.cowspent.theme.ThemeUtils
|
||||||
import net.helcel.cowspent.util.ICallback
|
import net.helcel.cowspent.util.ICallback
|
||||||
@@ -66,8 +67,10 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onBack = { finish() },
|
onBack = { finish() },
|
||||||
onSaveMain = { saveMainCurrency() },
|
onSaveMain = { saveMainCurrency() },
|
||||||
onAdd = { addCurrency() },
|
onAdd = { addOrUpdateCurrency() },
|
||||||
onDelete = { deleteCurrency(it) }
|
onDelete = { deleteCurrency(it) },
|
||||||
|
onEdit = { startEditing(it) },
|
||||||
|
onCancelEdit = { cancelEditing() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,11 +89,17 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
|||||||
val project = db!!.getProject(selectedProjectID)
|
val project = db!!.getProject(selectedProjectID)
|
||||||
if (project != null) {
|
if (project != null) {
|
||||||
db!!.syncIfRemote(project)
|
db!!.syncIfRemote(project)
|
||||||
withContext(Dispatchers.Main) {
|
if (project.type == ProjectType.COSPEND) {
|
||||||
if (!db!!.cowspentServerSyncHelper
|
withContext(Dispatchers.Main) {
|
||||||
.editRemoteProject(selectedProjectID, project.name, null, null, newMainCurrencyName, editMainCurrencyCallBack)
|
if (!db!!.cowspentServerSyncHelper
|
||||||
) {
|
.editRemoteProject(selectedProjectID, project.name, null, null, newMainCurrencyName, editMainCurrencyCallBack)
|
||||||
showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG)
|
) {
|
||||||
|
showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
showToast(this@ManageCurrenciesActivity, getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,22 +107,44 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addCurrency() {
|
private fun addOrUpdateCurrency() {
|
||||||
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
|
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
|
||||||
val newCurrency = DBCurrency(
|
val currencyName = viewModel.newCurrencyName
|
||||||
0, 0, selectedProjectID,
|
val editingId = viewModel.editingCurrencyId
|
||||||
viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
|
|
||||||
)
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
db!!.addCurrencyAndSync(newCurrency)
|
if (editingId != null) {
|
||||||
|
db!!.updateCurrency(editingId, currencyName, exchangeRate)
|
||||||
|
val currency = db!!.getCurrency(editingId)
|
||||||
|
if (currency != null) {
|
||||||
|
db!!.setCurrencyStateSync(editingId, DBBill.STATE_EDITED)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val newCurrency = DBCurrency(
|
||||||
|
0, 0, selectedProjectID,
|
||||||
|
currencyName, exchangeRate, DBBill.STATE_ADDED
|
||||||
|
)
|
||||||
|
db!!.addCurrencyAndSync(newCurrency)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
viewModel.newCurrencyName = ""
|
cancelEditing()
|
||||||
viewModel.newCurrencyRate = ""
|
|
||||||
updateCurrenciesList()
|
updateCurrenciesList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startEditing(currency: DBCurrency) {
|
||||||
|
viewModel.editingCurrencyId = currency.id
|
||||||
|
viewModel.newCurrencyName = currency.name ?: ""
|
||||||
|
viewModel.newCurrencyRate = currency.exchangeRate.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelEditing() {
|
||||||
|
viewModel.editingCurrencyId = null
|
||||||
|
viewModel.newCurrencyName = ""
|
||||||
|
viewModel.newCurrencyRate = ""
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteCurrency(currency: DBCurrency) {
|
private fun deleteCurrency(currency: DBCurrency) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
+220
-52
@@ -1,21 +1,36 @@
|
|||||||
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
|
||||||
|
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.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
|
||||||
@@ -24,7 +39,9 @@ fun ManageCurrenciesScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSaveMain: () -> Unit,
|
onSaveMain: () -> Unit,
|
||||||
onAdd: () -> Unit,
|
onAdd: () -> Unit,
|
||||||
onDelete: (DBCurrency) -> Unit
|
onDelete: (DBCurrency) -> Unit,
|
||||||
|
onEdit: (DBCurrency) -> Unit,
|
||||||
|
onCancelEdit: () -> Unit
|
||||||
) {
|
) {
|
||||||
val dialogState = viewModel.dialogState
|
val dialogState = viewModel.dialogState
|
||||||
if (dialogState != null) {
|
if (dialogState != null) {
|
||||||
@@ -73,55 +90,151 @@ fun ManageCurrenciesScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(16.dp)
|
.imePadding()
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.02f))
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6)
|
LaunchedEffect(viewModel.mainCurrencyName) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
if (viewModel.mainCurrencyName.isNotEmpty()) {
|
||||||
OutlinedTextField(
|
delay(500)
|
||||||
value = viewModel.mainCurrencyName,
|
onSaveMain()
|
||||||
onValueChange = { viewModel.mainCurrencyName = it },
|
|
||||||
label = { Text(stringResource(R.string.currency_edit_name)) },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Button(onClick = onSaveMain, enabled = viewModel.mainCurrencyName.isNotEmpty()) {
|
|
||||||
Text(stringResource(R.string.save_or_discard_bill_dialog_save))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
// Section 1: Main Currency
|
||||||
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
|
Text(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
text = stringResource(R.string.main_currency).uppercase(),
|
||||||
OutlinedTextField(
|
style = MaterialTheme.typography.overline,
|
||||||
value = viewModel.newCurrencyName,
|
color = MaterialTheme.colors.primary,
|
||||||
onValueChange = { viewModel.newCurrencyName = it },
|
fontWeight = FontWeight.Bold
|
||||||
label = { Text(stringResource(R.string.currency_edit_name)) },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
OutlinedTextField(
|
Card(
|
||||||
value = viewModel.newCurrencyRate,
|
shape = RoundedCornerShape(12.dp),
|
||||||
onValueChange = { viewModel.newCurrencyRate = it },
|
elevation = 2.dp,
|
||||||
label = { Text(stringResource(R.string.currency_rate)) },
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.weight(1f)
|
) {
|
||||||
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Button(
|
LazyColumn(
|
||||||
onClick = onAdd,
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
|
||||||
enabled = viewModel.isAddEnabled()
|
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 ->
|
items(viewModel.currencies) { currency ->
|
||||||
CurrencyRow(currency, onDelete = { onDelete(currency) })
|
CurrencyRow(
|
||||||
|
currency = currency,
|
||||||
|
mainCurrencyName = viewModel.mainCurrencyName,
|
||||||
|
isEditing = viewModel.editingCurrencyId == currency.id,
|
||||||
|
onEdit = { onEdit(currency) },
|
||||||
|
onDelete = { onDelete(currency) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,20 +242,70 @@ fun ManageCurrenciesScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CurrencyRow(currency: DBCurrency, onDelete: () -> Unit) {
|
fun CurrencyRow(
|
||||||
Row(
|
currency: DBCurrency,
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
mainCurrencyName: String,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
isEditing: Boolean,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
onDelete: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
elevation = if (isEditing) 4.dp else 1.dp,
|
||||||
|
border = if (isEditing) BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.5f)) else null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Row(
|
||||||
Text(currency.name ?: "", style = MaterialTheme.typography.subtitle1)
|
modifier = Modifier
|
||||||
Text("Rate: ${currency.exchangeRate}", style = MaterialTheme.typography.caption)
|
.fillMaxWidth()
|
||||||
}
|
.clickable(onClick = onEdit)
|
||||||
IconButton(onClick = onDelete) {
|
.padding(16.dp, 12.dp),
|
||||||
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "1 ",
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
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(
|
||||||
|
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)
|
||||||
@@ -151,6 +314,9 @@ fun CurrencyRowPreview() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
CurrencyRow(
|
CurrencyRow(
|
||||||
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
|
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
|
||||||
|
mainCurrencyName = "EUR",
|
||||||
|
isEditing = false,
|
||||||
|
onEdit = {},
|
||||||
onDelete = {}
|
onDelete = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -172,7 +338,9 @@ fun ManageCurrenciesScreenPreview() {
|
|||||||
onBack = {},
|
onBack = {},
|
||||||
onSaveMain = {},
|
onSaveMain = {},
|
||||||
onAdd = {},
|
onAdd = {},
|
||||||
onDelete = {}
|
onDelete = {},
|
||||||
|
onEdit = {},
|
||||||
|
onCancelEdit = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class ManageCurrenciesViewModel : ViewModel() {
|
|||||||
var newCurrencyName by mutableStateOf("")
|
var newCurrencyName by mutableStateOf("")
|
||||||
var newCurrencyRate by mutableStateOf("")
|
var newCurrencyRate by mutableStateOf("")
|
||||||
|
|
||||||
|
var editingCurrencyId by mutableStateOf<Long?>(null)
|
||||||
|
|
||||||
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
||||||
|
|
||||||
var dialogState by mutableStateOf<DialogState?>(null)
|
var dialogState by mutableStateOf<DialogState?>(null)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
@@ -23,9 +24,11 @@ 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 androidx.compose.ui.unit.sp
|
||||||
import net.helcel.cowspent.R
|
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.formatBalance
|
||||||
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
|
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
|
||||||
|
import net.helcel.cowspent.android.helper.TextIcon
|
||||||
|
import net.helcel.cowspent.android.helper.TextIconDisplay
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
import net.helcel.cowspent.model.DBProject
|
import net.helcel.cowspent.model.DBProject
|
||||||
import net.helcel.cowspent.model.ProjectType
|
import net.helcel.cowspent.model.ProjectType
|
||||||
@@ -38,6 +41,7 @@ fun Drawer(
|
|||||||
selectedProjectId: Long,
|
selectedProjectId: Long,
|
||||||
selectedMemberId: Long?,
|
selectedMemberId: Long?,
|
||||||
lastSyncText: String,
|
lastSyncText: String,
|
||||||
|
mainCurrency: String? = null,
|
||||||
showArchived: Boolean = false,
|
showArchived: Boolean = false,
|
||||||
onProjectClick: (Long) -> Unit,
|
onProjectClick: (Long) -> Unit,
|
||||||
onProjectOptionsClick: (Long) -> Unit,
|
onProjectOptionsClick: (Long) -> Unit,
|
||||||
@@ -96,22 +100,37 @@ fun Drawer(
|
|||||||
|
|
||||||
// Members Section
|
// Members Section
|
||||||
val membersState = rememberLazyListState()
|
val membersState = rememberLazyListState()
|
||||||
|
val sortedMembers = remember(members, memberBalances) {
|
||||||
|
members.sortedWith(compareBy<DBMember> {
|
||||||
|
val balance = memberBalances[it.id] ?: 0.0
|
||||||
|
when {
|
||||||
|
balance > 0.01 -> 0
|
||||||
|
balance < -0.01 -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
}.thenBy {
|
||||||
|
val balance = memberBalances[it.id] ?: 0.0
|
||||||
|
if (balance > 0.01) -balance else balance
|
||||||
|
}.thenBy { it.name })
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = membersState,
|
state = membersState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.heightIn(max = maxBottomHeight)
|
.heightIn(max = maxBottomHeight)
|
||||||
.lazyVerticalScrollbar(membersState)
|
.lazyVerticalScrollbar(membersState)
|
||||||
) {
|
) {
|
||||||
if (members.isNotEmpty()) {
|
if (sortedMembers.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
icon = Icons.Default.Receipt,
|
icon = Icons.Default.Receipt,
|
||||||
text = stringResource(R.string.label_all_bills),
|
text = stringResource(R.string.label_all_bills),
|
||||||
|
secondaryText = mainCurrency,
|
||||||
selected = selectedMemberId == null,
|
selected = selectedMemberId == null,
|
||||||
onClick = { onMemberClick(null) }
|
onClick = { onMemberClick(null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(members) { member ->
|
items(sortedMembers) { member ->
|
||||||
val balance = memberBalances[member.id] ?: 0.0
|
val balance = memberBalances[member.id] ?: 0.0
|
||||||
MemberDrawerItem(
|
MemberDrawerItem(
|
||||||
member = member,
|
member = member,
|
||||||
@@ -241,6 +260,7 @@ fun DrawerItem(
|
|||||||
member: DBMember? = null,
|
member: DBMember? = null,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
balanceText: String? = null,
|
balanceText: String? = null,
|
||||||
|
secondaryText: String? = null,
|
||||||
balanceColor: Color = Color.Unspecified,
|
balanceColor: Color = Color.Unspecified,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
alpha: Float = 1f,
|
alpha: Float = 1f,
|
||||||
@@ -259,13 +279,8 @@ fun DrawerItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
UserAvatar(
|
MemberAvatar(
|
||||||
name = member.name,
|
member = member,
|
||||||
r = member.r,
|
|
||||||
g = member.g,
|
|
||||||
b = member.b,
|
|
||||||
avatar = member.avatar,
|
|
||||||
disabled = !member.isActivated,
|
|
||||||
size = 24.dp,
|
size = 24.dp,
|
||||||
alpha = alpha
|
alpha = alpha
|
||||||
)
|
)
|
||||||
@@ -294,6 +309,17 @@ fun DrawerItem(
|
|||||||
modifier = Modifier.padding(end = 8.dp)
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (secondaryText != null) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
) {
|
||||||
|
TextIconDisplay(
|
||||||
|
textIcon = TextIcon.Symbol(secondaryText),
|
||||||
|
tint = contentColor.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (onSecondaryClick != null) {
|
if (onSecondaryClick != null) {
|
||||||
IconButton(onClick = onSecondaryClick) {
|
IconButton(onClick = onSecondaryClick) {
|
||||||
@@ -340,6 +366,7 @@ fun DrawerPreview() {
|
|||||||
selectedProjectId = 1,
|
selectedProjectId = 1,
|
||||||
selectedMemberId = null,
|
selectedMemberId = null,
|
||||||
lastSyncText = "Last sync: 5 mins ago",
|
lastSyncText = "Last sync: 5 mins ago",
|
||||||
|
mainCurrency = "EUR",
|
||||||
onProjectClick = {},
|
onProjectClick = {},
|
||||||
onProjectOptionsClick = {},
|
onProjectOptionsClick = {},
|
||||||
onMemberClick = {},
|
onMemberClick = {},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||||||
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.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
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
|
||||||
|
|
||||||
@@ -110,28 +111,55 @@ fun AlertDialog(
|
|||||||
if (message != null) Spacer(Modifier.height(8.dp))
|
if (message != null) Spacer(Modifier.height(8.dp))
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index, item ->
|
||||||
Row(
|
Column {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
onItemSelected?.invoke(index)
|
.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = text,
|
||||||
|
style = MaterialTheme.typography.body1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
Text(
|
if (index < items.size - 1) {
|
||||||
text = item.toString(),
|
Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
|
||||||
style = MaterialTheme.typography.body1
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,28 +244,55 @@ fun AlertDialogContent(
|
|||||||
if (message != null) Spacer(Modifier.height(8.dp))
|
if (message != null) Spacer(Modifier.height(8.dp))
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index, item ->
|
||||||
Row(
|
Column {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
onItemSelected?.invoke(index)
|
.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = text,
|
||||||
|
style = MaterialTheme.typography.body1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
Text(
|
if (index < items.size - 1) {
|
||||||
text = item.toString(),
|
Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
|
||||||
style = MaterialTheme.typography.body1
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,80 @@
|
|||||||
package net.helcel.cowspent.android.helper
|
package net.helcel.cowspent.android.helper
|
||||||
|
|
||||||
import net.helcel.cowspent.util.SupportUtil
|
import java.text.NumberFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats amount using k/M notation (e.g., 1.5k for 1500).
|
||||||
|
*/
|
||||||
fun formatShortValue(value: Double): String {
|
fun formatShortValue(value: Double): String {
|
||||||
|
val absValue = abs(value)
|
||||||
|
val sign = if (value < 0) "-" else ""
|
||||||
return when {
|
return when {
|
||||||
value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "")
|
absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M"
|
||||||
value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "")
|
absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k"
|
||||||
else -> String.format(Locale.ROOT, "%.0f", value)
|
else -> sign + formatAmount(absValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats balance with sign and locale-aware number formatting.
|
||||||
|
*/
|
||||||
fun formatBalance(balance: Double): String {
|
fun formatBalance(balance: Double): String {
|
||||||
val rbalance = round(abs(balance) * 100.0) / 100.0
|
val rbalance = round(abs(balance) * 100.0) / 100.0
|
||||||
val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else ""
|
val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else ""
|
||||||
return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(rbalance)}"
|
return if (rbalance == 0.0) "" else "$balanceSign${formatAmount(rbalance)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats amount using system locale or specified locale.
|
||||||
|
*/
|
||||||
|
fun formatAmount(value: Double, locale: Locale = Locale.getDefault()): String {
|
||||||
|
val formatter = NumberFormat.getNumberInstance(locale)
|
||||||
|
formatter.maximumFractionDigits = 2
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
return formatter.format(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses amount from string robustly.
|
||||||
|
* Tries common separators and cleans up non-numeric characters (except separators).
|
||||||
|
*/
|
||||||
|
fun parseAmount(input: String?): Double? {
|
||||||
|
if (input.isNullOrBlank()) return null
|
||||||
|
|
||||||
|
// Remove non-numeric characters except for delimiters and minus sign
|
||||||
|
val cleaned = input.replace(Regex("[^0-9,.-]"), "").trim()
|
||||||
|
|
||||||
|
if (cleaned.isEmpty()) return null
|
||||||
|
|
||||||
|
// If there's both a comma and a dot, we assume the last one is the decimal separator
|
||||||
|
val lastDot = cleaned.lastIndexOf('.')
|
||||||
|
val lastComma = cleaned.lastIndexOf(',')
|
||||||
|
|
||||||
|
return if (lastDot != -1 && lastComma != -1) {
|
||||||
|
if (lastDot > lastComma) {
|
||||||
|
// Dot is decimal, comma is thousands
|
||||||
|
cleaned.replace(",", "").toDoubleOrNull()
|
||||||
|
} else {
|
||||||
|
// Comma is decimal, dot is thousands
|
||||||
|
cleaned.replace(".", "").replace(",", ".").toDoubleOrNull()
|
||||||
|
}
|
||||||
|
} else if (lastComma != -1) {
|
||||||
|
// Only comma. Could be decimal or thousands.
|
||||||
|
if (cleaned.count { it == ',' } == 1) {
|
||||||
|
cleaned.replace(',', '.').toDoubleOrNull()
|
||||||
|
} else {
|
||||||
|
// Multiple commas -> thousands
|
||||||
|
cleaned.replace(",", "").toDoubleOrNull()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only dots or no separators
|
||||||
|
if (cleaned.count { it == '.' } > 1) {
|
||||||
|
cleaned.replace(".", "").toDoubleOrNull()
|
||||||
|
} else {
|
||||||
|
cleaned.toDoubleOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.helcel.cowspent.android.helper
|
||||||
|
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
||||||
|
sealed class TextIcon {
|
||||||
|
data class Symbol(val text: String) : TextIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper Composable to display a [TextIcon].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TextIconDisplay(
|
||||||
|
textIcon: TextIcon,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
tint: Color = LocalContentColor.current
|
||||||
|
) {
|
||||||
|
when (textIcon) {
|
||||||
|
is TextIcon.Symbol -> Text(
|
||||||
|
text = textIcon.text,
|
||||||
|
modifier = modifier,
|
||||||
|
color = tint,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import net.helcel.cowspent.model.DBMember
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAvatar(
|
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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAvatarPreview() {
|
fun UserAvatarPreview() {
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ 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.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.DBBill
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
import net.helcel.cowspent.util.SupportUtil
|
import net.helcel.cowspent.util.SupportUtil
|
||||||
@@ -83,13 +84,8 @@ fun BillItemRow(bill: DBBill, payer: DBMember?, onClick: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
if (payer != null) {
|
if (payer != null) {
|
||||||
Box {
|
Box {
|
||||||
UserAvatar(
|
MemberAvatar(
|
||||||
name = payer.name,
|
member = payer,
|
||||||
r = payer.r,
|
|
||||||
g = payer.g,
|
|
||||||
b = payer.b,
|
|
||||||
avatar = payer.avatar,
|
|
||||||
disabled = !payer.isActivated,
|
|
||||||
size = 40.dp
|
size = 40.dp
|
||||||
)
|
)
|
||||||
if (bill.repeat != null && bill.repeat != DBBill.NON_REPEATED) {
|
if (bill.repeat != null && bill.repeat != DBBill.NON_REPEATED) {
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ fun BillsListScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
|
val selectedProject = viewModel.projects.find { it.id == viewModel.selectedProjectId }
|
||||||
Drawer(
|
Drawer(
|
||||||
projects = viewModel.projects,
|
projects = viewModel.projects,
|
||||||
members = viewModel.members,
|
members = viewModel.members,
|
||||||
@@ -437,6 +438,7 @@ fun BillsListScreen(
|
|||||||
selectedProjectId = viewModel.selectedProjectId,
|
selectedProjectId = viewModel.selectedProjectId,
|
||||||
selectedMemberId = viewModel.selectedMemberId,
|
selectedMemberId = viewModel.selectedMemberId,
|
||||||
lastSyncText = viewModel.lastSyncText,
|
lastSyncText = viewModel.lastSyncText,
|
||||||
|
mainCurrency = selectedProject?.currencyName,
|
||||||
showArchived = showArchived,
|
showArchived = showArchived,
|
||||||
onProjectClick = {
|
onProjectClick = {
|
||||||
viewModel.selectedMemberId = null
|
viewModel.selectedMemberId = null
|
||||||
|
|||||||
@@ -444,11 +444,11 @@ class BillsListViewActivity :
|
|||||||
fun onManageCurrenciesClick(projectId: Long) {
|
fun onManageCurrenciesClick(projectId: Long) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
|
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
|
||||||
if (proj != null && proj.type == ProjectType.COSPEND) {
|
if (proj != null) {
|
||||||
startActivity(Intent(applicationContext, ManageCurrenciesActivity::class.java).apply {
|
startActivity(Intent(applicationContext, ManageCurrenciesActivity::class.java).apply {
|
||||||
putExtra(ManageCurrenciesActivity.EXTRA_PROJECT_ID, projectId)
|
putExtra(ManageCurrenciesActivity.EXTRA_PROJECT_ID, projectId)
|
||||||
})
|
})
|
||||||
} else showToast(this@BillsListViewActivity, getString(R.string.currency_management_unavailable))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import net.helcel.cowspent.model.*
|
|||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
import net.helcel.cowspent.theme.ThemeUtils
|
import net.helcel.cowspent.theme.ThemeUtils
|
||||||
import net.helcel.cowspent.util.*
|
import net.helcel.cowspent.util.*
|
||||||
|
import java.util.Locale
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import net.helcel.cowspent.model.ProjectType
|
import net.helcel.cowspent.model.ProjectType
|
||||||
@@ -60,7 +61,8 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
chooseFromNextcloud()
|
chooseFromNextcloud()
|
||||||
},
|
},
|
||||||
onOkPressed = { onPressOk() },
|
onOkPressed = { onPressOk() },
|
||||||
onBack = { finish() }
|
onBack = { finish() },
|
||||||
|
onFieldsChanged = { updateAuthStatus() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,8 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
viewModel.projectUrl = defaultNcUrl
|
viewModel.projectUrl = defaultNcUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAuthStatus()
|
||||||
|
|
||||||
val defaultProjectId = intent.getStringExtra(PARAM_DEFAULT_PROJECT_ID)
|
val defaultProjectId = intent.getStringExtra(PARAM_DEFAULT_PROJECT_ID)
|
||||||
if (defaultProjectId != null) {
|
if (defaultProjectId != null) {
|
||||||
@@ -139,6 +143,17 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateAuthStatus() {
|
||||||
|
val url = getFormattedUrl()
|
||||||
|
val fakeProj = DBProject(
|
||||||
|
0, "", "", "", url,
|
||||||
|
"", 0L, viewModel.projectType, 0L,
|
||||||
|
null, false, DBProject.ACCESS_LEVEL_UNKNOWN,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
viewModel.isAuthenticatedAccount = db.cowspentServerSyncHelper.canCreateAuthenticatedProject(fakeProj)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onPressOk() {
|
private fun onPressOk() {
|
||||||
val type = viewModel.projectType
|
val type = viewModel.projectType
|
||||||
val todoCreate = viewModel.whatTodoIsCreate
|
val todoCreate = viewModel.whatTodoIsCreate
|
||||||
@@ -186,7 +201,16 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createProject() {
|
private fun createProject() {
|
||||||
val isCospendScheme = isCospendSchemeLink(getFormattedUrl())
|
val url = getFormattedUrl()
|
||||||
|
val isCospendScheme = isCospendSchemeLink(url)
|
||||||
|
|
||||||
|
if (viewModel.whatTodoIsCreate && viewModel.projectId.isEmpty() && viewModel.projectName.isNotEmpty()) {
|
||||||
|
viewModel.projectId = viewModel.projectName.lowercase(Locale.ROOT)
|
||||||
|
.replace("[^a-z0-9]".toRegex(), "-")
|
||||||
|
.replace("-+".toRegex(), "-")
|
||||||
|
.trim('-')
|
||||||
|
}
|
||||||
|
|
||||||
val rid = viewModel.projectId
|
val rid = viewModel.projectId
|
||||||
if (!isCospendScheme && (rid == "" || rid.contains(",") || rid.contains("/"))) {
|
if (!isCospendScheme && (rid == "" || rid.contains(",") || rid.contains("/"))) {
|
||||||
showToast(getString(R.string.error_invalid_project_remote_id), Toast.LENGTH_LONG)
|
showToast(getString(R.string.error_invalid_project_remote_id), Toast.LENGTH_LONG)
|
||||||
@@ -194,7 +218,7 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.projectType != ProjectType.LOCAL && !isCospendScheme) {
|
if (viewModel.projectType != ProjectType.LOCAL && !isCospendScheme) {
|
||||||
if (!isValidUrl(getFormattedUrl())) {
|
if (!isValidUrl(url)) {
|
||||||
showToast("Invalid URL", Toast.LENGTH_SHORT)
|
showToast("Invalid URL", Toast.LENGTH_SHORT)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -217,7 +241,16 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
showToast("Invalid project name", Toast.LENGTH_SHORT)
|
showToast("Invalid project name", Toast.LENGTH_SHORT)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!SupportUtil.isValidEmail(viewModel.projectEmail)) {
|
|
||||||
|
val fakeProj = DBProject(
|
||||||
|
0, rid, "", viewModel.projectName, url,
|
||||||
|
viewModel.projectEmail, 0L, viewModel.projectType, 0L,
|
||||||
|
null, false, DBProject.ACCESS_LEVEL_UNKNOWN,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
val isAuthenticated = db.cowspentServerSyncHelper.canCreateAuthenticatedProject(fakeProj)
|
||||||
|
|
||||||
|
if (!isAuthenticated && !SupportUtil.isValidEmail(viewModel.projectEmail)) {
|
||||||
showToast("Invalid email", Toast.LENGTH_SHORT)
|
showToast("Invalid email", Toast.LENGTH_SHORT)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -226,7 +259,7 @@ class NewProjectActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (!db.cowspentServerSyncHelper.createRemoteProject(
|
if (!db.cowspentServerSyncHelper.createRemoteProject(
|
||||||
viewModel.projectId, viewModel.projectName,
|
viewModel.projectId, viewModel.projectName,
|
||||||
viewModel.projectEmail, viewModel.projectPassword, getFormattedUrl(), viewModel.projectType, createRemoteCallBack
|
viewModel.projectEmail, viewModel.projectPassword, url, viewModel.projectType, createRemoteCallBack
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
viewModel.isCreatingRemoteProject = false
|
viewModel.isCreatingRemoteProject = false
|
||||||
|
|||||||
@@ -32,8 +32,13 @@ fun NewProjectScreen(
|
|||||||
onImportFile: () -> Unit,
|
onImportFile: () -> Unit,
|
||||||
onChooseFromNextcloud: () -> Unit,
|
onChooseFromNextcloud: () -> Unit,
|
||||||
onOkPressed: () -> Unit,
|
onOkPressed: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
onFieldsChanged: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
LaunchedEffect(viewModel.projectUrl, viewModel.projectType) {
|
||||||
|
onFieldsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -58,6 +63,7 @@ fun NewProjectScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.imePadding()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -162,16 +168,18 @@ fun NewProjectScreen(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
if (!viewModel.whatTodoIsCreate || !viewModel.isAuthenticatedAccount || viewModel.projectType != ProjectType.COSPEND) {
|
||||||
value = viewModel.projectId,
|
OutlinedTextField(
|
||||||
onValueChange = { viewModel.projectId = it },
|
value = viewModel.projectId,
|
||||||
label = { Text(stringResource(R.string.setting_project_id)) },
|
onValueChange = { viewModel.projectId = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
label = { Text(stringResource(R.string.setting_project_id)) },
|
||||||
leadingIcon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null) }
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
leadingIcon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
if (viewModel.projectType != ProjectType.LOCAL && (!viewModel.whatTodoIsCreate || viewModel.projectType == ProjectType.IHATEMONEY)) {
|
if (viewModel.projectType != ProjectType.LOCAL && (!viewModel.whatTodoIsCreate || viewModel.projectType == ProjectType.IHATEMONEY)) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.projectPassword,
|
value = viewModel.projectPassword,
|
||||||
onValueChange = { viewModel.projectPassword = it },
|
onValueChange = { viewModel.projectPassword = it },
|
||||||
@@ -179,10 +187,10 @@ fun NewProjectScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
|
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.whatTodoIsCreate && viewModel.projectType != ProjectType.LOCAL) {
|
if (viewModel.whatTodoIsCreate && viewModel.projectType != ProjectType.LOCAL) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.projectName,
|
value = viewModel.projectName,
|
||||||
onValueChange = { viewModel.projectName = it },
|
onValueChange = { viewModel.projectName = it },
|
||||||
@@ -190,14 +198,17 @@ fun NewProjectScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
|
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
if (!viewModel.isAuthenticatedAccount || viewModel.projectType == ProjectType.IHATEMONEY) {
|
||||||
value = viewModel.projectEmail,
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
onValueChange = { viewModel.projectEmail = it },
|
OutlinedTextField(
|
||||||
label = { Text(stringResource(R.string.setting_new_project_email)) },
|
value = viewModel.projectEmail,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
onValueChange = { viewModel.projectEmail = it },
|
||||||
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
|
label = { Text(stringResource(R.string.setting_new_project_email)) },
|
||||||
)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,6 +345,7 @@ fun NewProjectScreenPreview() {
|
|||||||
onImportFile = {},
|
onImportFile = {},
|
||||||
onChooseFromNextcloud = {},
|
onChooseFromNextcloud = {},
|
||||||
onOkPressed = {},
|
onOkPressed = {},
|
||||||
onBack = {}
|
onBack = {},
|
||||||
|
onFieldsChanged = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-4
@@ -30,6 +30,8 @@ class NewProjectViewModel : ViewModel() {
|
|||||||
var projectName by mutableStateOf("")
|
var projectName by mutableStateOf("")
|
||||||
var projectEmail by mutableStateOf("")
|
var projectEmail by mutableStateOf("")
|
||||||
|
|
||||||
|
var isAuthenticatedAccount by mutableStateOf(false)
|
||||||
|
|
||||||
var showAuthWarningDialog by mutableStateOf(false)
|
var showAuthWarningDialog by mutableStateOf(false)
|
||||||
var showNextcloudProjectDialog by mutableStateOf(false)
|
var showNextcloudProjectDialog by mutableStateOf(false)
|
||||||
var nextcloudProjects by mutableStateOf<List<DBAccountProject>>(emptyList())
|
var nextcloudProjects by mutableStateOf<List<DBAccountProject>>(emptyList())
|
||||||
@@ -39,12 +41,23 @@ class NewProjectViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun isFormValid(): Boolean {
|
fun isFormValid(): Boolean {
|
||||||
if (whatTodoIsCreate) {
|
if (whatTodoIsCreate) {
|
||||||
if (projectId.isEmpty()) return false
|
if (projectType == ProjectType.LOCAL) {
|
||||||
if (projectType != ProjectType.LOCAL) {
|
return projectId.isNotEmpty()
|
||||||
|
} else {
|
||||||
if (projectUrl.isEmpty()) return false
|
if (projectUrl.isEmpty()) return false
|
||||||
if (projectName.isEmpty()) return false
|
if (projectName.isEmpty()) return false
|
||||||
if (projectEmail.isEmpty()) return false
|
|
||||||
if (projectType == ProjectType.IHATEMONEY && projectPassword.isEmpty()) return false
|
if (projectType == ProjectType.COSPEND && isAuthenticatedAccount) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId.isEmpty()) return false
|
||||||
|
if (projectType == ProjectType.IHATEMONEY) {
|
||||||
|
if (projectEmail.isEmpty()) return false
|
||||||
|
if (projectPassword.isEmpty()) return false
|
||||||
|
} else {
|
||||||
|
if (projectEmail.isEmpty()) return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Join
|
// Join
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package net.helcel.cowspent.android.project.edit
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
@@ -80,11 +82,14 @@ fun EditProjectScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.imePadding()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.name,
|
value = viewModel.name,
|
||||||
|
|||||||
+3
-8
@@ -30,7 +30,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
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 net.helcel.cowspent.R
|
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.android.helper.lazyVerticalScrollbar
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
|
|
||||||
@@ -84,13 +84,8 @@ fun MemberManagementDialogContent(
|
|||||||
.padding(vertical = 12.dp),
|
.padding(vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
UserAvatar(
|
MemberAvatar(
|
||||||
name = member.name,
|
member = member,
|
||||||
r = member.r,
|
|
||||||
g = member.g,
|
|
||||||
b = member.b,
|
|
||||||
avatar = member.avatar,
|
|
||||||
disabled = !member.isActivated,
|
|
||||||
size = 40.dp
|
size = 40.dp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import androidx.core.graphics.toColorInt
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
import net.helcel.cowspent.android.helper.EditableExposedDropdownMenu
|
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.android.helper.formatShortValue
|
||||||
import net.helcel.cowspent.model.DBBill
|
import net.helcel.cowspent.model.DBBill
|
||||||
import net.helcel.cowspent.model.DBCategory
|
import net.helcel.cowspent.model.DBCategory
|
||||||
@@ -170,7 +170,7 @@ fun ProjectSankeyDiagram(
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||||
if (selectedMember != null) {
|
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)
|
} else Icon(Icons.Default.Group, contentDescription = null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,7 +182,7 @@ fun ProjectSankeyDiagram(
|
|||||||
}
|
}
|
||||||
allMembers.forEach { member ->
|
allMembers.forEach { member ->
|
||||||
DropdownMenuItem(onClick = { selectedMemberId = member.id; expanded = false }) {
|
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))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(member.name)
|
Text(member.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,7 +553,11 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrencies(projectId: Long): List<DBCurrency> {
|
fun getCurrencies(projectId: Long): List<DBCurrency> {
|
||||||
return getCurrenciesCustom("$key_projectid = ?", arrayOf(projectId.toString()), null)
|
return getCurrenciesCustom(
|
||||||
|
"$key_projectid = ? AND $key_state != ?",
|
||||||
|
arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()),
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|||||||
@@ -320,48 +320,69 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED)
|
if (project.type == ProjectType.COSPEND) {
|
||||||
for (cToDel in currenciesToDelete) {
|
val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED)
|
||||||
try {
|
for (cToDel in currenciesToDelete) {
|
||||||
val deleteRemoteCurrencyResponse = client!!.deleteRemoteCurrency(project, cToDel.remoteId)
|
try {
|
||||||
if (deleteRemoteCurrencyResponse.stringContent == "OK") {
|
val deleteRemoteCurrencyResponse = client!!.deleteRemoteCurrency(project, cToDel.remoteId)
|
||||||
Log.d(TAG, "successfully deleted currency on remote project : delete it locally")
|
if (deleteRemoteCurrencyResponse.stringContent == "OK") {
|
||||||
dbHelper.deleteCurrency(cToDel.id)
|
Log.d(TAG, "successfully deleted currency on remote project : delete it locally")
|
||||||
}
|
dbHelper.deleteCurrency(cToDel.id)
|
||||||
} catch (e: IOException) {
|
}
|
||||||
if (e.message == "\"Not Found\"") {
|
} catch (e: IOException) {
|
||||||
Log.d(TAG, "failed to delete currency on remote project : delete it locally anyway")
|
if (e.message == "\"Not Found\"") {
|
||||||
dbHelper.deleteCurrency(cToDel.id)
|
Log.d(TAG, "failed to delete currency on remote project : delete it locally anyway")
|
||||||
} else {
|
dbHelper.deleteCurrency(cToDel.id)
|
||||||
throw e
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED)
|
||||||
|
for (cToDel in currenciesToDelete) {
|
||||||
|
dbHelper.deleteCurrency(cToDel.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED)
|
if (project.type == ProjectType.COSPEND) {
|
||||||
for (cToEdit in currenciesToEdit) {
|
val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED)
|
||||||
try {
|
for (cToEdit in currenciesToEdit) {
|
||||||
val editRemoteCurrencyResponse = client!!.editRemoteCurrency(project, cToEdit)
|
try {
|
||||||
if (editRemoteCurrencyResponse.stringContent == cToEdit.remoteId.toString()) {
|
val editRemoteCurrencyResponse = client!!.editRemoteCurrency(project, cToEdit)
|
||||||
dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK)
|
if (editRemoteCurrencyResponse.stringContent == cToEdit.remoteId.toString()) {
|
||||||
Log.d(TAG, "SUCCESSFUL remote currency edition (${editRemoteCurrencyResponse.stringContent})")
|
dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK)
|
||||||
} else {
|
Log.d(TAG, "SUCCESSFUL remote currency edition (${editRemoteCurrencyResponse.stringContent})")
|
||||||
Log.d(TAG, "FAILED to edit remote currency (${editRemoteCurrencyResponse.stringContent})")
|
} else {
|
||||||
}
|
Log.d(TAG, "FAILED to edit remote currency (${editRemoteCurrencyResponse.stringContent})")
|
||||||
} catch (e: IOException) {
|
}
|
||||||
if (e.message == "{\"message\": \"Internal Server Error\"}") {
|
} catch (e: IOException) {
|
||||||
Log.d(TAG, "FAILED to edit remote currency : it does not exist remotely")
|
if (e.message == "{\"message\": \"Internal Server Error\"}") {
|
||||||
} else {
|
Log.d(TAG, "FAILED to edit remote currency : it does not exist remotely")
|
||||||
throw e
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED)
|
||||||
|
for (cToEdit in currenciesToEdit) {
|
||||||
|
dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
|
if (project.type == ProjectType.COSPEND) {
|
||||||
for (cToAdd in currencyToAdd) {
|
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
|
||||||
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
|
for (cToAdd in currencyToAdd) {
|
||||||
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong()
|
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
|
||||||
if (newRemoteId > 0) {
|
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong()
|
||||||
|
if (newRemoteId > 0) {
|
||||||
|
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
|
||||||
|
for (cToAdd in currencyToAdd) {
|
||||||
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
|
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ object ThemeUtils {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
|
.imePadding()
|
||||||
) {
|
) {
|
||||||
content()
|
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="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="#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"/>
|
<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>
|
||||||
<group android:name="line">
|
<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"/>
|
<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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
Reference in New Issue
Block a user