Compare commits
5
Commits
488aa9bba6
...
dc469ee576
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc469ee576
|
||
|
|
e59680e710
|
||
|
|
fff3490719
|
||
|
|
aedefc9010
|
||
|
|
492230faa2
|
@@ -12,6 +12,7 @@ import androidx.activity.viewModels
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -151,12 +152,25 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0)
|
val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0)
|
||||||
val timeNowSeconds = System.currentTimeMillis() / 1000
|
val timeNowSeconds = System.currentTimeMillis() / 1000
|
||||||
if (billIdToDuplicate == 0L) {
|
if (billIdToDuplicate == 0L) {
|
||||||
val project = db.getProject(projectId)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
bill = DBBill(
|
val fillFromLast = preferences.getBoolean(getString(R.string.pref_key_fill_new_bill_from_last), false)
|
||||||
0, 0, projectId, project?.lastPayerId ?: 0, 0.0, timeNowSeconds,
|
val lastBill = if (fillFromLast) db.getLastBillOfProject(projectId) else null
|
||||||
"", DBBill.STATE_ADDED, DBBill.NON_REPEATED,
|
|
||||||
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE
|
if (lastBill != null) {
|
||||||
)
|
bill = DBBill(
|
||||||
|
0, 0, projectId, lastBill.payerId, 0.0, timeNowSeconds,
|
||||||
|
"", DBBill.STATE_ADDED, lastBill.repeat ?: DBBill.NON_REPEATED,
|
||||||
|
lastBill.paymentMode, lastBill.categoryId, "", lastBill.paymentModeId
|
||||||
|
)
|
||||||
|
bill.billOwers = lastBill.billOwers.map { DBBillOwer(0, 0, it.memberId) }
|
||||||
|
} else {
|
||||||
|
val project = db.getProject(projectId)
|
||||||
|
bill = DBBill(
|
||||||
|
0, 0, projectId, project?.lastPayerId ?: 0, 0.0, timeNowSeconds,
|
||||||
|
"", DBBill.STATE_ADDED, DBBill.NON_REPEATED,
|
||||||
|
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val btd = db.getBill(billIdToDuplicate)!!
|
val btd = db.getBill(billIdToDuplicate)!!
|
||||||
bill = DBBill(
|
bill = DBBill(
|
||||||
@@ -333,7 +347,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
viewModel.owersSelection[id] == true && viewModel.parseAmountFromUi(amountStr) > 0
|
viewModel.owersSelection[id] == true && viewModel.parseAmountFromUi(amountStr) > 0
|
||||||
}.mapValues {
|
}.mapValues {
|
||||||
val uiAmount = viewModel.parseAmountFromUi(it.value)
|
val uiAmount = viewModel.parseAmountFromUi(it.value)
|
||||||
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
|
SupportUtil.round2(uiAmount * viewModel.selectedCurrencyRate)
|
||||||
}
|
}
|
||||||
} else { // PERCENT
|
} else { // PERCENT
|
||||||
val totalAmount = viewModel.amountAsDouble
|
val totalAmount = viewModel.amountAsDouble
|
||||||
@@ -342,7 +356,7 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
}.mapValues {
|
}.mapValues {
|
||||||
val percent = viewModel.parseAmountFromUi(it.value)
|
val percent = viewModel.parseAmountFromUi(it.value)
|
||||||
val uiAmount = SupportUtil.round2(totalAmount * percent / 100.0)
|
val uiAmount = SupportUtil.round2(totalAmount * percent / 100.0)
|
||||||
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
|
SupportUtil.round2(uiAmount * viewModel.selectedCurrencyRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -450,7 +450,10 @@ fun OwerSelectionSection(
|
|||||||
size = 32.dp
|
size = 32.dp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(member.name, modifier = Modifier.weight(1f))
|
val weightSuffix = if (viewModel.hasDifferentWeights && member.weight != 1.0) {
|
||||||
|
" (x${member.weight.toString().removeSuffix(".0")})"
|
||||||
|
} else ""
|
||||||
|
Text("${member.name}$weightSuffix", modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
if (isSelected || viewModel.splitMode != SplitMode.EVEN) {
|
if (isSelected || viewModel.splitMode != SplitMode.EVEN) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -671,19 +674,115 @@ fun BillAdditionalDetailsSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewModelConstructorInComposable")
|
@SuppressLint("ViewModelConstructorInComposable")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true, name = "Standard Split")
|
||||||
@Composable
|
@Composable
|
||||||
fun EditBillScreenPreview() {
|
fun EditBillScreenPreview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
EditBillScreen(
|
EditBillScreen(
|
||||||
viewModel = EditBillViewModel().apply {
|
viewModel = EditBillViewModel().apply {
|
||||||
what = "Pizza"
|
what = "Pizza"
|
||||||
amount = "12.50"
|
amount = "12.00"
|
||||||
mainCurrencyName = "EUR"
|
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)
|
||||||
)
|
)
|
||||||
|
owersSelection[1] = true
|
||||||
|
owersSelection[2] = true
|
||||||
|
updateSplits()
|
||||||
|
},
|
||||||
|
categories = emptyList(),
|
||||||
|
paymentModes = emptyList(),
|
||||||
|
onSave = {},
|
||||||
|
onBack = {},
|
||||||
|
onDateClick = {},
|
||||||
|
onTimeClick = {},
|
||||||
|
onScan = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewModelConstructorInComposable")
|
||||||
|
@Preview(showBackground = true, name = "Weighted Split")
|
||||||
|
@Composable
|
||||||
|
fun EditBillScreenWeightedPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
EditBillScreen(
|
||||||
|
viewModel = EditBillViewModel().apply {
|
||||||
|
what = "Weighted Pizza"
|
||||||
|
amount = "60.00"
|
||||||
|
mainCurrencyName = "EUR"
|
||||||
|
members = listOf(
|
||||||
|
DBMember(1, 0, 0, "Alice", true, 2.0, 0, null, null, null, null, null),
|
||||||
|
DBMember(2, 0, 0, "Bob", true, 1.0, 0, null, null, null, null, null),
|
||||||
|
DBMember(3, 0, 0, "Charlie", true, 1.0, 0, null, null, null, null, null)
|
||||||
|
)
|
||||||
|
owersSelection[1] = true
|
||||||
|
owersSelection[2] = true
|
||||||
|
owersSelection[3] = true
|
||||||
|
updateSplits()
|
||||||
|
},
|
||||||
|
categories = emptyList(),
|
||||||
|
paymentModes = emptyList(),
|
||||||
|
onSave = {},
|
||||||
|
onBack = {},
|
||||||
|
onDateClick = {},
|
||||||
|
onTimeClick = {},
|
||||||
|
onScan = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewModelConstructorInComposable")
|
||||||
|
@Preview(showBackground = true, name = "Custom Split (#)")
|
||||||
|
@Composable
|
||||||
|
fun EditBillScreenCustomPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
EditBillScreen(
|
||||||
|
viewModel = EditBillViewModel().apply {
|
||||||
|
what = "Custom Split Pizza"
|
||||||
|
amount = "60.00"
|
||||||
|
mainCurrencyName = "EUR"
|
||||||
|
members = listOf(
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
splitMode = SplitMode.CUSTOM
|
||||||
|
owersSelection[1] = true
|
||||||
|
owersSelection[2] = true
|
||||||
|
owersCustomSplit[1] = "40.00"
|
||||||
|
owersCustomSplit[2] = "20.00"
|
||||||
|
},
|
||||||
|
categories = emptyList(),
|
||||||
|
paymentModes = emptyList(),
|
||||||
|
onSave = {},
|
||||||
|
onBack = {},
|
||||||
|
onDateClick = {},
|
||||||
|
onTimeClick = {},
|
||||||
|
onScan = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewModelConstructorInComposable")
|
||||||
|
@Preview(showBackground = true, name = "Percent Split (%)")
|
||||||
|
@Composable
|
||||||
|
fun EditBillScreenPercentPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
EditBillScreen(
|
||||||
|
viewModel = EditBillViewModel().apply {
|
||||||
|
what = "Percent Split Pizza"
|
||||||
|
amount = "100.00"
|
||||||
|
mainCurrencyName = "EUR"
|
||||||
|
members = listOf(
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
splitMode = SplitMode.PERCENT
|
||||||
|
owersSelection[1] = true
|
||||||
|
owersSelection[2] = true
|
||||||
|
owersPercentSplit[1] = "70"
|
||||||
|
owersPercentSplit[2] = "30"
|
||||||
},
|
},
|
||||||
categories = emptyList(),
|
categories = emptyList(),
|
||||||
paymentModes = emptyList(),
|
paymentModes = emptyList(),
|
||||||
|
|||||||
@@ -61,18 +61,18 @@ class EditBillViewModel : ViewModel() {
|
|||||||
val amountAsDouble: Double
|
val amountAsDouble: Double
|
||||||
get() = parseAmountFromUi(amount)
|
get() = parseAmountFromUi(amount)
|
||||||
|
|
||||||
fun getEvenSplit(): Double {
|
val hasDifferentWeights: Boolean
|
||||||
val selectedOwersCount = owersSelection.count { it.value }
|
get() = members.isNotEmpty() && members.any { it.weight != members[0].weight }
|
||||||
return if (selectedOwersCount > 0) SupportUtil.round2(amountAsDouble / selectedOwersCount) else 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSplits() {
|
fun updateSplits() {
|
||||||
if (splitMode == SplitMode.EVEN) {
|
if (splitMode == SplitMode.EVEN) {
|
||||||
val even = getEvenSplit()
|
val selectedMembers = members.filter { owersSelection[it.id] == true }
|
||||||
val evenStr = if (even == 0.0) "" else SupportUtil.round2(even).toString()
|
val totalWeight = selectedMembers.sumOf { it.weight }
|
||||||
|
val uiAmount = amountAsDouble
|
||||||
members.forEach { m ->
|
members.forEach { m ->
|
||||||
if (owersSelection[m.id] == true) {
|
if (owersSelection[m.id] == true) {
|
||||||
owersCustomSplit[m.id] = evenStr
|
val share = if (totalWeight > 0) (uiAmount * m.weight) / totalWeight else 0.0
|
||||||
|
owersCustomSplit[m.id] = if (share == 0.0) "" else SupportUtil.round2(share).toString()
|
||||||
} else {
|
} else {
|
||||||
owersCustomSplit.remove(m.id)
|
owersCustomSplit.remove(m.id)
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ class EditBillViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFinalAmount(): Double {
|
fun getFinalAmount(): Double {
|
||||||
return SupportUtil.round2(amountAsDouble / selectedCurrencyRate)
|
return SupportUtil.round2(amountAsDouble * selectedCurrencyRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFinalComment(): String {
|
fun getFinalComment(): String {
|
||||||
@@ -252,10 +252,9 @@ class EditBillViewModel : ViewModel() {
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
// If we have metadata, the custom splits from DB are also converted.
|
// If we have metadata, the custom splits from DB are also converted.
|
||||||
// We should show them as "Original" if possible?
|
// We should show them as "Original" if possible?
|
||||||
// Actually, if we use metadata, we should probably store original splits too,
|
// Reverse calculation for display: UI Part = DB Part / Rate
|
||||||
// but for now let's just reverse the rate for display.
|
|
||||||
val dbPart = customSplits[member.id]!!
|
val dbPart = customSplits[member.id]!!
|
||||||
val uiPart = if (selectedCurrencyRate != 1.0) dbPart * selectedCurrencyRate else dbPart
|
val uiPart = if (selectedCurrencyRate != 0.0) dbPart / selectedCurrencyRate else dbPart
|
||||||
owersCustomSplit[member.id] = SupportUtil.round2(uiPart).toString()
|
owersCustomSplit[member.id] = SupportUtil.round2(uiPart).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,18 +262,18 @@ class EditBillViewModel : ViewModel() {
|
|||||||
// Even split logic
|
// Even split logic
|
||||||
splitMode = SplitMode.EVEN
|
splitMode = SplitMode.EVEN
|
||||||
val billOwerIds = bill.billOwersIds
|
val billOwerIds = bill.billOwersIds
|
||||||
val selectedCount = billOwerIds.size
|
val selectedMembers = members.filter { billOwerIds.contains(it.id) }
|
||||||
|
val totalWeight = selectedMembers.sumOf { it.weight }
|
||||||
|
|
||||||
// Use UI amount for even split calculation
|
// Use UI amount for even split calculation
|
||||||
val uiAmount = amountAsDouble
|
val uiAmount = amountAsDouble
|
||||||
val evenSplit = if (selectedCount > 0) uiAmount / selectedCount else 0.0
|
|
||||||
val evenSplitStr = if (evenSplit == 0.0) "" else SupportUtil.round2(evenSplit).toString()
|
|
||||||
|
|
||||||
for (member in members) {
|
for (member in members) {
|
||||||
val selected = billOwerIds.contains(member.id)
|
val selected = billOwerIds.contains(member.id)
|
||||||
owersSelection[member.id] = selected
|
owersSelection[member.id] = selected
|
||||||
if (selected) {
|
if (selected) {
|
||||||
owersCustomSplit[member.id] = evenSplitStr
|
val share = if (totalWeight > 0) (uiAmount * member.weight) / totalWeight else 0.0
|
||||||
|
owersCustomSplit[member.id] = if (share == 0.0) "" else SupportUtil.round2(share).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -114,7 +114,8 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addOrUpdateCurrency() {
|
private fun addOrUpdateCurrency() {
|
||||||
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
|
val uiRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
|
||||||
|
val exchangeRate = if (uiRate != 0.0) 1.0 / uiRate else 0.0
|
||||||
val currencyName = viewModel.newCurrencyName
|
val currencyName = viewModel.newCurrencyName
|
||||||
val editingId = viewModel.editingCurrencyId
|
val editingId = viewModel.editingCurrencyId
|
||||||
|
|
||||||
@@ -142,7 +143,8 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
|||||||
private fun startEditing(currency: DBCurrency) {
|
private fun startEditing(currency: DBCurrency) {
|
||||||
viewModel.editingCurrencyId = currency.id
|
viewModel.editingCurrencyId = currency.id
|
||||||
viewModel.newCurrencyName = currency.name ?: ""
|
viewModel.newCurrencyName = currency.name ?: ""
|
||||||
viewModel.newCurrencyRate = currency.exchangeRate.toString()
|
val uiRate = if (currency.exchangeRate != 0.0) 1.0 / currency.exchangeRate else 0.0
|
||||||
|
viewModel.newCurrencyRate = uiRate.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelEditing() {
|
private fun cancelEditing() {
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ fun CurrencyRow(
|
|||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = formatAmount(currency.exchangeRate),
|
text = formatAmount(if (currency.exchangeRate != 0.0) 1.0 / currency.exchangeRate else 0.0),
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
|
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
|
||||||
|
|||||||
@@ -761,7 +761,7 @@ class BillsListViewActivity :
|
|||||||
val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) }
|
val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) }
|
||||||
if (proj != null && !proj.isLocal) {
|
if (proj != null && !proj.isLocal) {
|
||||||
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
|
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
|
||||||
db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId)
|
db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId, manual)
|
||||||
} else viewModel.isRefreshing = false
|
} else viewModel.isRefreshing = false
|
||||||
}
|
}
|
||||||
} else viewModel.isRefreshing = false
|
} else viewModel.isRefreshing = false
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import androidx.preference.PreferenceManager
|
|||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
import net.helcel.cowspent.android.helper.ColorPicker
|
import net.helcel.cowspent.android.helper.ColorPicker
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
|
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
|
||||||
import net.helcel.cowspent.util.ColorUtils
|
import net.helcel.cowspent.util.ColorUtils
|
||||||
import net.helcel.cowspent.util.Cowspent
|
import net.helcel.cowspent.util.Cowspent
|
||||||
|
|
||||||
@@ -80,22 +81,28 @@ fun SettingsScreen(
|
|||||||
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
|
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
|
||||||
val keyShowArchived = stringResource(R.string.pref_key_show_archived)
|
val keyShowArchived = stringResource(R.string.pref_key_show_archived)
|
||||||
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
|
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
|
||||||
|
val keyFillNewBillFromLast = stringResource(R.string.pref_key_fill_new_bill_from_last)
|
||||||
|
|
||||||
|
val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context)
|
||||||
|
|
||||||
// States for preferences
|
// States for preferences
|
||||||
var nightMode by remember(keyNightMode) {
|
var nightMode by remember(keyNightMode) {
|
||||||
mutableStateOf(sharedPreferences.getString(keyNightMode, "-1") ?: "-1")
|
mutableStateOf(sharedPreferences.getString(keyNightMode, "-1") ?: "-1")
|
||||||
}
|
}
|
||||||
|
|
||||||
var colorMode by remember(keyColorMode, keyUseServerColor, keyUseSystemColor) {
|
var colorMode by remember(keyColorMode, keyUseServerColor, keyUseSystemColor, isNextcloudConfigured) {
|
||||||
mutableStateOf(sharedPreferences.getString(keyColorMode, null) ?: run {
|
val savedMode = sharedPreferences.getString(keyColorMode, null)
|
||||||
val useServer = sharedPreferences.getBoolean(keyUseServerColor, true)
|
val mode = savedMode ?: run {
|
||||||
|
val useServer = sharedPreferences.getBoolean(keyUseServerColor, false)
|
||||||
val useSystem = sharedPreferences.getBoolean(keyUseSystemColor, true)
|
val useSystem = sharedPreferences.getBoolean(keyUseSystemColor, true)
|
||||||
when {
|
when {
|
||||||
useServer -> "server"
|
useServer && isNextcloudConfigured -> "server"
|
||||||
useSystem -> "system"
|
useSystem -> "system"
|
||||||
else -> "manual"
|
else -> "manual"
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
// Fallback to system if server is selected but not configured
|
||||||
|
mutableStateOf(if (mode == "server" && !isNextcloudConfigured) "system" else mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply theme globally only when leaving settings to avoid flickering/restarts during selection
|
// Apply theme globally only when leaving settings to avoid flickering/restarts during selection
|
||||||
@@ -117,6 +124,9 @@ fun SettingsScreen(
|
|||||||
var betaFeatures by remember(keyBetaFeatures) {
|
var betaFeatures by remember(keyBetaFeatures) {
|
||||||
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
|
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
|
||||||
}
|
}
|
||||||
|
var fillNewBillFromLast by remember(keyFillNewBillFromLast) {
|
||||||
|
mutableStateOf(sharedPreferences.getBoolean(keyFillNewBillFromLast, false))
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -181,15 +191,19 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val colorModeEntries = buildMap {
|
||||||
|
put("system", stringResource(R.string.pref_value_color_system))
|
||||||
|
if (isNextcloudConfigured) {
|
||||||
|
put("server", stringResource(R.string.pref_value_color_server))
|
||||||
|
}
|
||||||
|
put("manual", stringResource(R.string.pref_value_color_manual))
|
||||||
|
}
|
||||||
|
|
||||||
SettingsListPreference(
|
SettingsListPreference(
|
||||||
title = stringResource(R.string.settings_color_mode),
|
title = stringResource(R.string.settings_color_mode),
|
||||||
icon = Icons.Default.Palette,
|
icon = Icons.Default.Palette,
|
||||||
value = colorMode,
|
value = colorMode,
|
||||||
entries = mapOf(
|
entries = colorModeEntries,
|
||||||
"system" to stringResource(R.string.pref_value_color_system),
|
|
||||||
"server" to stringResource(R.string.pref_value_color_server),
|
|
||||||
"manual" to stringResource(R.string.pref_value_color_manual)
|
|
||||||
),
|
|
||||||
onValueChange = { mode ->
|
onValueChange = { mode ->
|
||||||
colorMode = mode
|
colorMode = mode
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
@@ -259,6 +273,21 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (betaFeatures) {
|
||||||
|
SettingsSwitchPreference(
|
||||||
|
title = stringResource(R.string.settings_fill_new_bill_from_last),
|
||||||
|
summary = stringResource(R.string.settings_fill_new_bill_from_last_summary),
|
||||||
|
icon = Icons.Default.Info,
|
||||||
|
checked = fillNewBillFromLast,
|
||||||
|
onCheckedChange = {
|
||||||
|
fillNewBillFromLast = it
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putBoolean(keyFillNewBillFromLast, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsPreference(
|
SettingsPreference(
|
||||||
title = stringResource(R.string.title_about),
|
title = stringResource(R.string.title_about),
|
||||||
icon = Icons.Default.Info,
|
icon = Icons.Default.Info,
|
||||||
|
|||||||
@@ -557,6 +557,15 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
return if (bills.isEmpty()) null else bills[0]
|
return if (bills.isEmpty()) null else bills[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLastBillOfProject(projectId: Long): DBBill? {
|
||||||
|
val list = getBillsCustom(
|
||||||
|
"$key_projectid = ? AND $key_state != ?",
|
||||||
|
arrayOf(projectId.toString(), DBBill.STATE_DELETED.toString()),
|
||||||
|
"$key_id DESC LIMIT 1"
|
||||||
|
)
|
||||||
|
return if (list.isEmpty()) null else list[0]
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun searchBills(query: CharSequence?, projectId: Long): List<DBBill> {
|
fun searchBills(query: CharSequence?, projectId: Long): List<DBBill> {
|
||||||
val andWhere: MutableList<String> = ArrayList()
|
val andWhere: MutableList<String> = ArrayList()
|
||||||
|
|||||||
@@ -93,14 +93,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
callbacksPull.add(callback)
|
callbacksPull.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scheduleSync(onlyLocalChanges: Boolean, projId: Long): SyncTask? {
|
fun scheduleSync(onlyLocalChanges: Boolean, projId: Long, forceFullSync: Boolean = false): SyncTask? {
|
||||||
Log.d(TAG, "Sync requested (${if (onlyLocalChanges) "onlyLocalChanges" else "full"}; ${if (syncActive) "sync active" else "sync NOT active"}) ...")
|
Log.d(TAG, "Sync requested (${if (onlyLocalChanges) "onlyLocalChanges" else "full"}; ${if (syncActive) "sync active" else "sync NOT active"}; forceFullSync=$forceFullSync) ...")
|
||||||
updateNetworkStatus()
|
updateNetworkStatus()
|
||||||
if (isSyncPossible && (!syncActive || onlyLocalChanges)) {
|
if (isSyncPossible && (!syncActive || onlyLocalChanges)) {
|
||||||
val project = dbHelper.getProject(projId)
|
val project = dbHelper.getProject(projId)
|
||||||
if (project != null) {
|
if (project != null) {
|
||||||
Log.d(TAG, "... starting now")
|
Log.d(TAG, "... starting now")
|
||||||
val syncTask = SyncTask(onlyLocalChanges, project)
|
val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync)
|
||||||
syncTask.addCallbacks(callbacksPush)
|
syncTask.addCallbacks(callbacksPush)
|
||||||
callbacksPush = ArrayList()
|
callbacksPush = ArrayList()
|
||||||
if (!onlyLocalChanges) {
|
if (!onlyLocalChanges) {
|
||||||
@@ -138,7 +138,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject) {
|
inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject, private val forceFullSync: Boolean = false) {
|
||||||
private val callbacks: MutableList<ICallback> = ArrayList()
|
private val callbacks: MutableList<ICallback> = ArrayList()
|
||||||
private var nextcloudClient: NextcloudClient? = null
|
private var nextcloudClient: NextcloudClient? = null
|
||||||
private var client: VersatileProjectSyncClient? = null
|
private var client: VersatileProjectSyncClient? = null
|
||||||
@@ -726,23 +726,59 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap()
|
val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap()
|
||||||
dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id }
|
dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id }
|
||||||
|
|
||||||
val billsResponse = client!!.getBills(project)
|
|
||||||
val isIHM = project.type == ProjectType.IHATEMONEY
|
val isIHM = project.type == ProjectType.IHATEMONEY
|
||||||
val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
|
var serverSyncTimestamp = project.lastSyncedTimestamp
|
||||||
val remoteBills: List<DBBill> = if (isIHM) {
|
val remoteBills = mutableListOf<DBBill>()
|
||||||
billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
|
val remoteAllBillIds = mutableListOf<Long>()
|
||||||
|
|
||||||
|
val localBills = dbHelper.getBillsOfProject(project.id)
|
||||||
|
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
|
||||||
|
|
||||||
|
if (project.type == ProjectType.COSPEND && !forceFullSync && localBills.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "Starting partial sync for project ${project.remoteId}")
|
||||||
|
var offset = 0
|
||||||
|
val limit = 50
|
||||||
|
var allMatched = false
|
||||||
|
while (!allMatched) {
|
||||||
|
val partialResponse = client!!.getBills(project, offset, limit, true, 0)
|
||||||
|
val partialRemoteBills = partialResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
|
||||||
|
if (partialRemoteBills.isEmpty()) break
|
||||||
|
|
||||||
|
remoteBills.addAll(partialRemoteBills)
|
||||||
|
|
||||||
|
var frameMatched = true
|
||||||
|
for (rb in partialRemoteBills) {
|
||||||
|
val lb = localBillsByRemoteId[rb.remoteId]
|
||||||
|
if (lb == null || hasChanged(lb, rb)) {
|
||||||
|
frameMatched = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == 0 && partialResponse.syncTimestamp > 0) {
|
||||||
|
serverSyncTimestamp = partialResponse.syncTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameMatched) {
|
||||||
|
allMatched = true
|
||||||
|
} else {
|
||||||
|
offset += limit
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
|
Log.d(TAG, "Starting full sync for project ${project.remoteId}")
|
||||||
}
|
val billsResponse = client!!.getBills(project)
|
||||||
val remoteAllBillIds: List<Long> = if (isIHM) {
|
serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
|
||||||
remoteBills.map { it.remoteId }
|
if (isIHM) {
|
||||||
} else {
|
remoteBills.addAll(billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId))
|
||||||
billsResponse.allBillIds
|
remoteAllBillIds.addAll(remoteBills.map { it.remoteId })
|
||||||
|
} else {
|
||||||
|
remoteBills.addAll(billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId))
|
||||||
|
remoteAllBillIds.addAll(billsResponse.allBillIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val remoteBillsByRemoteId = remoteBills.associateBy { it.remoteId }
|
val remoteBillsByRemoteId = remoteBills.associateBy { it.remoteId }
|
||||||
val localBills = dbHelper.getBillsOfProject(project.id)
|
|
||||||
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
|
|
||||||
|
|
||||||
for (remoteBill in remoteBills) {
|
for (remoteBill in remoteBills) {
|
||||||
if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) {
|
if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) {
|
||||||
@@ -783,7 +819,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.type == ProjectType.COSPEND || project.type == ProjectType.IHATEMONEY) {
|
if (remoteAllBillIds.isNotEmpty() && (project.type == ProjectType.COSPEND || project.type == ProjectType.IHATEMONEY)) {
|
||||||
for (localBill in localBills) {
|
for (localBill in localBills) {
|
||||||
if (!remoteAllBillIds.contains(localBill.remoteId)) {
|
if (!remoteAllBillIds.contains(localBill.remoteId)) {
|
||||||
dbHelper.deleteBill(localBill.id)
|
dbHelper.deleteBill(localBill.id)
|
||||||
@@ -792,7 +828,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
Log.d(TAG, "Delete local bill : $localBill")
|
Log.d(TAG, "Delete local bill : $localBill")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (remoteAllBillIds.isNotEmpty()) {
|
||||||
for (localBill in localBills) {
|
for (localBill in localBills) {
|
||||||
if (!remoteBillsByRemoteId.containsKey(localBill.remoteId)) {
|
if (!remoteBillsByRemoteId.containsKey(localBill.remoteId)) {
|
||||||
dbHelper.deleteBill(localBill.id)
|
dbHelper.deleteBill(localBill.id)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ object ColorUtils {
|
|||||||
val colorMode = if (prefs.contains(modeKey)) {
|
val colorMode = if (prefs.contains(modeKey)) {
|
||||||
prefs.getString(modeKey, "system")
|
prefs.getString(modeKey, "system")
|
||||||
} else {
|
} else {
|
||||||
val useServer = prefs.getBoolean(context.getString(R.string.pref_key_use_server_color), true)
|
val useServer = prefs.getBoolean(context.getString(R.string.pref_key_use_server_color), false)
|
||||||
val useSystem = prefs.getBoolean(context.getString(R.string.pref_key_use_system_color), true)
|
val useSystem = prefs.getBoolean(context.getString(R.string.pref_key_use_system_color), true)
|
||||||
when {
|
when {
|
||||||
useServer -> "server"
|
useServer -> "server"
|
||||||
|
|||||||
@@ -158,10 +158,18 @@ class VersatileProjectSyncClient(
|
|||||||
paramKeys.add("password")
|
paramKeys.add("password")
|
||||||
paramValues.add(newPassword)
|
paramValues.add(newPassword)
|
||||||
}
|
}
|
||||||
|
if (newEmail != null) {
|
||||||
|
paramKeys.add("contact_email")
|
||||||
|
paramValues.add(newEmail)
|
||||||
|
}
|
||||||
if (newMainCurrencyName != null) {
|
if (newMainCurrencyName != null) {
|
||||||
paramKeys.add("currencyName")
|
paramKeys.add("currencyName")
|
||||||
paramValues.add(newMainCurrencyName)
|
paramValues.add(newMainCurrencyName)
|
||||||
}
|
}
|
||||||
|
if (newArchivedTs != null) {
|
||||||
|
paramKeys.add("archived_ts")
|
||||||
|
paramValues.add(newArchivedTs.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (canAccessProjectWithNCLogin(project)) {
|
if (canAccessProjectWithNCLogin(project)) {
|
||||||
username = this.username
|
username = this.username
|
||||||
@@ -679,22 +687,66 @@ class VersatileProjectSyncClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
|
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
|
||||||
fun getBills(project: DBProject): ServerResponse.BillsResponse {
|
fun getBills(
|
||||||
|
project: DBProject,
|
||||||
|
offset: Int? = null,
|
||||||
|
limit: Int? = null,
|
||||||
|
reverse: Boolean? = null,
|
||||||
|
deleted: Int? = null
|
||||||
|
): ServerResponse.BillsResponse {
|
||||||
var target: String
|
var target: String
|
||||||
var username: String?
|
var username: String?
|
||||||
var password: String?
|
var password: String?
|
||||||
var bearerToken: String?
|
var bearerToken: String?
|
||||||
var useOcsApiRequest: Boolean
|
var useOcsApiRequest: Boolean
|
||||||
|
|
||||||
|
val paramKeys: MutableList<String> = ArrayList()
|
||||||
|
val paramValues: MutableList<String> = ArrayList()
|
||||||
|
|
||||||
if (ProjectType.COSPEND == project.type) {
|
if (ProjectType.COSPEND == project.type) {
|
||||||
val tsLastSync = project.lastSyncedTimestamp
|
val tsLastSync = project.lastSyncedTimestamp
|
||||||
|
if (offset == null) {
|
||||||
|
if (cospendVersionGT161) {
|
||||||
|
paramKeys.add("lastChanged")
|
||||||
|
} else {
|
||||||
|
paramKeys.add("lastchanged")
|
||||||
|
}
|
||||||
|
paramValues.add(tsLastSync.toString())
|
||||||
|
} else {
|
||||||
|
paramKeys.add("offset")
|
||||||
|
paramValues.add(offset.toString())
|
||||||
|
if (limit != null) {
|
||||||
|
paramKeys.add("limit")
|
||||||
|
paramValues.add(limit.toString())
|
||||||
|
}
|
||||||
|
if (reverse != null) {
|
||||||
|
paramKeys.add("reverse")
|
||||||
|
paramValues.add(reverse.toString())
|
||||||
|
}
|
||||||
|
if (deleted != null) {
|
||||||
|
paramKeys.add("deleted")
|
||||||
|
paramValues.add(deleted.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (canAccessProjectWithNCLogin(project)) {
|
if (canAccessProjectWithNCLogin(project)) {
|
||||||
username = this.username
|
username = this.username
|
||||||
password = this.password
|
password = this.password
|
||||||
target = if (cospendVersionGT161)
|
|
||||||
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/bills?lastChanged=" + tsLastSync
|
|
||||||
else
|
|
||||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills?lastchanged=" + tsLastSync
|
|
||||||
useOcsApiRequest = cospendVersionGT161
|
useOcsApiRequest = cospendVersionGT161
|
||||||
|
val baseUrl = project.getRequestBaseUrl(useOcsApiRequest)
|
||||||
|
target = if (useOcsApiRequest)
|
||||||
|
"$baseUrl/api/v1/projects/${project.remoteId}/bills"
|
||||||
|
else
|
||||||
|
"$baseUrl/api-priv/projects/${project.remoteId}/bills"
|
||||||
|
|
||||||
|
if (paramKeys.isNotEmpty()) {
|
||||||
|
target += "?"
|
||||||
|
for (i in paramKeys.indices) {
|
||||||
|
if (i > 0) target += "&"
|
||||||
|
target += "${paramKeys[i]}=${paramValues[i]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ServerResponse.BillsResponse(
|
return ServerResponse.BillsResponse(
|
||||||
requestServer(
|
requestServer(
|
||||||
target, METHOD_GET, null, null,
|
target, METHOD_GET, null, null,
|
||||||
@@ -703,14 +755,6 @@ class VersatileProjectSyncClient(
|
|||||||
useOcsApiRequest
|
useOcsApiRequest
|
||||||
)
|
)
|
||||||
} else if (canAccessProjectWithSSO(project)) {
|
} else if (canAccessProjectWithSSO(project)) {
|
||||||
val paramKeys: MutableList<String> = ArrayList()
|
|
||||||
val paramValues: MutableList<String> = ArrayList()
|
|
||||||
if (cospendVersionGT161) {
|
|
||||||
paramKeys.add("lastChanged")
|
|
||||||
} else {
|
|
||||||
paramKeys.add("lastchanged")
|
|
||||||
}
|
|
||||||
paramValues.add(tsLastSync.toString())
|
|
||||||
return if (cospendVersionGT161) {
|
return if (cospendVersionGT161) {
|
||||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
|
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
|
||||||
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
|
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
|
||||||
@@ -720,10 +764,20 @@ class VersatileProjectSyncClient(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
useOcsApiRequest = cospendVersionGT161
|
useOcsApiRequest = cospendVersionGT161
|
||||||
target = if (cospendVersionGT161)
|
val baseUrl = project.getRequestBaseUrl(useOcsApiRequest)
|
||||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync
|
target = if (useOcsApiRequest)
|
||||||
|
"$baseUrl/api/v1/public/projects/${project.remoteId}/${getEncodedPassword(project.password)}/bills"
|
||||||
else
|
else
|
||||||
project.getRequestBaseUrl(false) + "/apiv2/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastchanged=" + tsLastSync
|
"$baseUrl/apiv2/projects/${project.remoteId}/${getEncodedPassword(project.password)}/bills"
|
||||||
|
|
||||||
|
if (paramKeys.isNotEmpty()) {
|
||||||
|
target += "?"
|
||||||
|
for (i in paramKeys.indices) {
|
||||||
|
if (i > 0) target += "&"
|
||||||
|
target += "${paramKeys[i]}=${paramValues[i]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ServerResponse.BillsResponse(
|
return ServerResponse.BillsResponse(
|
||||||
requestServer(
|
requestServer(
|
||||||
target, METHOD_GET, null, null,
|
target, METHOD_GET, null, null,
|
||||||
|
|||||||
@@ -137,6 +137,8 @@
|
|||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
<string name="settings_beta_features">Beta Features</string>
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
<string name="settings_colorpicker_title">Choose Color</string>
|
<string name="settings_colorpicker_title">Choose Color</string>
|
||||||
|
|
||||||
@@ -158,6 +160,7 @@
|
|||||||
<string name="pref_key_offline_mode" translatable="false">offlineMode</string>
|
<string name="pref_key_offline_mode" translatable="false">offlineMode</string>
|
||||||
<string name="pref_key_show_archived" translatable="false">showArchived</string>
|
<string name="pref_key_show_archived" translatable="false">showArchived</string>
|
||||||
<string name="pref_key_beta_features" translatable="false">betaFeatures</string>
|
<string name="pref_key_beta_features" translatable="false">betaFeatures</string>
|
||||||
|
<string name="pref_key_fill_new_bill_from_last" translatable="false">fillNewBillFromLast</string>
|
||||||
<string name="pref_value_night_mode_no" translatable="false">1</string>
|
<string name="pref_value_night_mode_no" translatable="false">1</string>
|
||||||
<string name="pref_value_night_mode_yes" translatable="false">2</string>
|
<string name="pref_value_night_mode_yes" translatable="false">2</string>
|
||||||
<string name="pref_value_night_mode_system" translatable="false">-1</string>
|
<string name="pref_value_night_mode_system" translatable="false">-1</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user