From a5c825c8e0c47ba506cbc8de0b3584915b779b2c Mon Sep 17 00:00:00 2001 From: soraefir Date: Sat, 14 Nov 2026 13:31:48 +0100 Subject: [PATCH] better prefill, autocategorize --- .../android/bill_edit/EditBillActivity.kt | 150 +++++++++++++--- .../android/bill_edit/EditBillScreen.kt | 78 ++++++++- .../android/bill_edit/EditBillViewModel.kt | 76 +++++++- .../android/bill_label/LabelBillsViewModel.kt | 22 +-- .../android/settings/SettingsScreen.kt | 35 ++-- .../persistence/CowspentSQLiteOpenHelper.kt | 2 + .../net/helcel/cowspent/util/BillPrefill.kt | 163 ++++++++++++++++++ .../helcel/cowspent/util/CategorySuggester.kt | 64 +++++++ app/src/main/res/values-af-rZA/strings.xml | 2 - app/src/main/res/values-ar-rSA/strings.xml | 2 - app/src/main/res/values-ca-rES/strings.xml | 2 - app/src/main/res/values-cs-rCZ/strings.xml | 2 - app/src/main/res/values-da-rDK/strings.xml | 2 - app/src/main/res/values-de-rDE/strings.xml | 2 - app/src/main/res/values-el-rGR/strings.xml | 2 - app/src/main/res/values-es-rES/strings.xml | 2 - app/src/main/res/values-fi-rFI/strings.xml | 2 - app/src/main/res/values-fr-rFR/strings.xml | 2 - app/src/main/res/values-hu-rHU/strings.xml | 2 - app/src/main/res/values-it-rIT/strings.xml | 2 - app/src/main/res/values-iw-rIL/strings.xml | 2 - app/src/main/res/values-ja-rJP/strings.xml | 2 - app/src/main/res/values-ko-rKR/strings.xml | 2 - app/src/main/res/values-nl-rNL/strings.xml | 2 - app/src/main/res/values-no-rNO/strings.xml | 2 - app/src/main/res/values-pl-rPL/strings.xml | 2 - app/src/main/res/values-pt-rBR/strings.xml | 2 - app/src/main/res/values-pt-rPT/strings.xml | 2 - app/src/main/res/values-ro-rRO/strings.xml | 2 - app/src/main/res/values-ru-rRU/strings.xml | 2 - app/src/main/res/values-sr-rSP/strings.xml | 2 - app/src/main/res/values-sv-rSE/strings.xml | 2 - app/src/main/res/values-tr-rTR/strings.xml | 2 - app/src/main/res/values-uk-rUA/strings.xml | 2 - app/src/main/res/values-vi-rVN/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values-zh-rTW/strings.xml | 2 - app/src/main/res/values/strings.xml | 17 +- 38 files changed, 527 insertions(+), 138 deletions(-) create mode 100644 app/src/main/java/net/helcel/cowspent/util/BillPrefill.kt create mode 100644 app/src/main/java/net/helcel/cowspent/util/CategorySuggester.kt diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt index 2f2356d..2dc6c4e 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.annotation.WorkerThread import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.remember import androidx.lifecycle.lifecycleScope @@ -27,6 +28,10 @@ import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.util.BillParser +import net.helcel.cowspent.util.BillPrefill +import net.helcel.cowspent.util.BillTemplate +import net.helcel.cowspent.util.CategorySuggester +import net.helcel.cowspent.util.PrefillMode import net.helcel.cowspent.util.SupportUtil import java.text.ParseException import java.time.ZoneId @@ -40,6 +45,9 @@ class EditBillActivity : AppCompatActivity() { private var projectType: ProjectType = ProjectType.LOCAL private val calendar = Calendar.getInstance() + // The pre-fill mode the form was opened with; null when the bill is not a fresh one. + private var prefillMode: PrefillMode? = null + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -95,8 +103,14 @@ class EditBillActivity : AppCompatActivity() { val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java) scanQRCodeLauncher.launch(createIntent) }, - onDuplicate = if (!viewModel.isNewBill) { { duplicateCurrentBill() } } else null, - onDelete = if (!viewModel.isNewBill) { { deleteBillAsked() } } else null, + onDuplicate = if (!viewModel.isNewBill) { + { duplicateCurrentBill() } + } else null, + onDelete = if (!viewModel.isNewBill) { + { deleteBillAsked() } + } else null, + onPrefillModeSelected = { onPrefillModeSelected(it) }, + onSavePrefillPreset = { onSavePrefillPreset() }, accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN ) } @@ -133,7 +147,7 @@ class EditBillActivity : AppCompatActivity() { first.timestamp, first.what, first.state, first.repeat, first.paymentMode, first.categoryId, first.comment, first.paymentModeId ) - + val splits = mutableMapOf() for (b in sourceBills) { val owersCount = b.billOwers.size @@ -152,25 +166,13 @@ class EditBillActivity : AppCompatActivity() { val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0) val timeNowSeconds = System.currentTimeMillis() / 1000 if (billIdToDuplicate == 0L) { - val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) - val fillFromLast = preferences.getBoolean(getString(R.string.pref_key_fill_new_bill_from_last), false) - val lastBill = if (fillFromLast) db.getLastBillOfProject(projectId) else null - - 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 - ) - } + val mode = BillPrefill.mode(applicationContext, projectId) + bill = newBill( + projectId, + timeNowSeconds, + BillPrefill.templateFor(applicationContext, db, projectId, mode) + ) + prefillMode = mode } else { val btd = db.getBill(billIdToDuplicate)!! bill = DBBill( @@ -193,23 +195,107 @@ class EditBillActivity : AppCompatActivity() { val members = allMembers.filter { m -> m.isActivated || m.id == bill.payerId || billOwerIds.contains(m.id) } - + val project = db.getProject(bill.projectId) val currencies = db.getCurrencies(bill.projectId) - + if (project != null) { db.ensureDefaultLabels(bill.projectId, project.type) } + val hasPreset = BillPrefill.preset(applicationContext, bill.projectId) != null + val categoryMatcher = if (bill.id == 0L) autoCategoryMatcher(bill.projectId) else null + withContext(Dispatchers.Main) { viewModel.currencies = currencies viewModel.mainCurrencyName = project?.currencyName ?: "" viewModel.isNewBill = (bill.id == 0L) + viewModel.prefillMode = prefillMode + viewModel.hasPrefillPreset = hasPreset viewModel.initFromBill(bill, members, customSplits) + categoryMatcher?.let { viewModel.enableAutoCategorize(it) } } } } + @WorkerThread + private fun autoCategoryMatcher(projectId: Long): ((String) -> Long?)? { + val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + // The switch lives under Extra Features; with those off it is hidden, so it must not act. + if (!preferences.getBoolean(getString(R.string.pref_key_extra_features), false)) return null + if (!preferences.getBoolean(getString(R.string.pref_key_auto_categorize), false)) return null + val categorized = db.getBillsOfProject(projectId) + .filter { it.categoryId != DBBill.CATEGORY_NONE && it.state != DBBill.STATE_DELETED } + // Only categories the picker offers: the project's own, plus the built-in reimbursement. + val allowed = db.getCategories(projectId).map { it.id }.toSet() + DBBill.CATEGORY_REIMBURSEMENT + return { what -> CategorySuggester.confidentMatch(what, categorized, allowed) } + } + + @WorkerThread + private fun newBill(projectId: Long, timestamp: Long, template: BillTemplate?): DBBill { + val project = db.getProject(projectId) + val lastPayerId = project?.lastPayerId ?: 0 + if (template == null) { + return DBBill( + 0, 0, projectId, lastPayerId, 0.0, timestamp, + "", DBBill.STATE_ADDED, DBBill.NON_REPEATED, + DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE + ) + } + val activeIds = db.getMembersOfProject(projectId, null).filter { it.isActivated }.map { it.id }.toSet() + return DBBill( + 0, 0, projectId, if (template.payerId in activeIds) template.payerId else lastPayerId, + 0.0, timestamp, "", DBBill.STATE_ADDED, template.repeat, + template.paymentMode, template.categoryId, "", template.paymentModeId + ).apply { + billOwers = template.owerIds.filter { it in activeIds }.map { DBBillOwer(0, 0, it) } + } + } + + private fun onPrefillModeSelected(mode: PrefillMode) { + val projectId = bill.projectId + lifecycleScope.launch { + val template = withContext(Dispatchers.IO) { + BillPrefill.setMode(applicationContext, projectId, mode) + newBill(projectId, bill.timestamp, BillPrefill.templateFor(applicationContext, db, projectId, mode)) + } + // Keep the reference bill in step, so the unsaved-changes check compares against + // what pre-filling put there rather than flagging it as the user's edit. + bill.payerId = template.payerId + bill.billOwers = template.billOwers + bill.categoryId = template.categoryId + bill.paymentMode = template.paymentMode + bill.paymentModeId = template.paymentModeId + bill.repeat = template.repeat + viewModel.prefillMode = mode + viewModel.applyPrefill(template) + } + } + + private fun currentTemplate() = BillTemplate( + payerId = viewModel.payerId, + owerIds = viewModel.getOwersIds(), + categoryId = viewModel.categoryId, + paymentModeId = viewModel.paymentModeId, + paymentMode = bill.paymentMode, + repeat = viewModel.repeat + ) + + /** Saves the form's current values as the project's preset and switches to using it. */ + private fun onSavePrefillPreset() { + val projectId = bill.projectId + val template = currentTemplate() + lifecycleScope.launch { + withContext(Dispatchers.IO) { + BillPrefill.savePreset(applicationContext, projectId, template) + BillPrefill.setMode(applicationContext, projectId, PrefillMode.PRESET) + } + viewModel.hasPrefillPreset = true + viewModel.prefillMode = PrefillMode.PRESET + showToast(this@EditBillActivity, getString(R.string.prefill_preset_saved)) + } + } + private val scanQRCodeLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK && result.data != null) { @@ -251,7 +337,7 @@ class EditBillActivity : AppCompatActivity() { calendar.timeInMillis = System.currentTimeMillis() viewModel.timestamp = calendar.timeInMillis / 1000 viewModel.isNewBill = true - + showToast(this, "Duplicating bill...") } @@ -282,8 +368,18 @@ class EditBillActivity : AppCompatActivity() { if (validationError != null) { showToast(this, validationError) } else { + // Taken before saving: saving can split the bill into one per ower, and the template + // should hold the whole selection rather than whichever split was written last. + val createdHere = bill.id == 0L + val template = currentTemplate() lifecycleScope.launch { - val savedBillId = withContext(Dispatchers.IO) { saveBill() } + val savedBillId = withContext(Dispatchers.IO) { + saveBill().also { id -> + if (createdHere && id != 0L) { + BillPrefill.recordCreatedOnDevice(applicationContext, bill.projectId, template) + } + } + } val data = Intent() data.putExtra(MainConstants.SAVED_BILL_ID, savedBillId) setResult(RESULT_OK, data) @@ -361,7 +457,7 @@ class EditBillActivity : AppCompatActivity() { } if (splits.isEmpty()) return@withContext 0L - + val finalComment = viewModel.getFinalComment() val splitEntries = splits.entries.toList() diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt index c5ab47e..b284735 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt @@ -49,6 +49,7 @@ import net.helcel.cowspent.R import net.helcel.cowspent.android.helper.* import net.helcel.cowspent.model.* import net.helcel.cowspent.util.CategoryUtils +import net.helcel.cowspent.util.PrefillMode import net.helcel.cowspent.util.SupportUtil import java.util.Date import kotlin.math.abs @@ -66,6 +67,8 @@ fun EditBillScreen( onScan: () -> Unit, onDuplicate: (() -> Unit)? = null, onDelete: (() -> Unit)? = null, + onPrefillModeSelected: (PrefillMode) -> Unit = {}, + onSavePrefillPreset: () -> Unit = {}, accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN ) { val canEdit = accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN || accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT @@ -102,6 +105,14 @@ fun EditBillScreen( }, actions = { if (canEdit) { + viewModel.prefillMode?.let { mode -> + PrefillMenu( + mode = mode, + hasPreset = viewModel.hasPrefillPreset, + onModeSelected = onPrefillModeSelected, + onSavePreset = onSavePrefillPreset + ) + } IconButton(onClick = onScan) { Icon(Icons.Default.QrCodeScanner, contentDescription = null) } @@ -218,6 +229,65 @@ fun EditBillScreen( private val MathOperatorRowHeight = 54.dp +/** + * Chooses where this project's new bills take their payer, owers, category and payment mode + * from, and saves the form's current values as the project's preset. + */ +@Composable +fun PrefillMenu( + mode: PrefillMode, + hasPreset: Boolean, + onModeSelected: (PrefillMode) -> Unit, + onSavePreset: () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + Box { + IconButton(onClick = { expanded = true }) { + Icon(Icons.Default.AutoFixHigh, contentDescription = stringResource(R.string.prefill_button)) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + val options = listOf( + PrefillMode.OFF to R.string.prefill_mode_off, + PrefillMode.LATEST_IN_PROJECT to R.string.prefill_mode_latest_project, + PrefillMode.LATEST_ON_DEVICE to R.string.prefill_mode_latest_device, + PrefillMode.PRESET to R.string.prefill_mode_preset + ) + for ((option, label) in options) { + // The preset has nothing to fill from until one is saved. + val enabled = option != PrefillMode.PRESET || hasPreset + DropdownMenuItem( + enabled = enabled, + onClick = { + expanded = false + onModeSelected(option) + } + ) { + RadioButton(selected = option == mode, onClick = null, enabled = enabled) + Spacer(Modifier.width(12.dp)) + Column { + Text(stringResource(label)) + if (!enabled) { + Text( + text = stringResource(R.string.prefill_mode_preset_empty), + style = MaterialTheme.typography.caption + ) + } + } + } + } + Divider(modifier = Modifier.padding(vertical = 4.dp)) + DropdownMenuItem(onClick = { + expanded = false + onSavePreset() + }) { + Icon(Icons.Default.Save, contentDescription = null) + Spacer(Modifier.width(12.dp)) + Text(stringResource(R.string.prefill_save_preset)) + } + } + } +} + private data class KeyboardColors(val tray: Color, val edge: Color, val key: Color, val label: Color) @Composable @@ -382,7 +452,7 @@ fun BillBasicInfoSection( OutlinedTextField( value = viewModel.what, - onValueChange = { viewModel.what = it }, + onValueChange = { viewModel.updateWhat(it) }, enabled = canEdit, placeholder = { Text(stringResource(R.string.label_what)) }, modifier = Modifier.fillMaxWidth(), @@ -686,7 +756,7 @@ fun BillAdditionalDetailsSection( }, content = { DropdownMenuItem(onClick = { - viewModel.categoryId = 0 + viewModel.selectCategory(DBBill.CATEGORY_NONE) categoryExpanded = false }) { Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null) @@ -695,7 +765,7 @@ fun BillAdditionalDetailsSection( } DropdownMenuItem(onClick = { - viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT + viewModel.selectCategory(DBBill.CATEGORY_REIMBURSEMENT) categoryExpanded = false }) { Text(text = "\uD83D\uDCB0", fontSize = 20.sp) @@ -705,7 +775,7 @@ fun BillAdditionalDetailsSection( categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category -> DropdownMenuItem(onClick = { - viewModel.categoryId = category.id + viewModel.selectCategory(category.id) categoryExpanded = false }) { Text(text = category.icon, fontSize = 20.sp) diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt index 7db3ebf..7b292f5 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt @@ -13,6 +13,7 @@ import net.helcel.cowspent.android.helper.parseAmount import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBCurrency import net.helcel.cowspent.model.DBMember +import net.helcel.cowspent.util.PrefillMode import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.evalMath @@ -31,6 +32,20 @@ class EditBillViewModel : ViewModel() { var categoryId by mutableLongStateOf(0L) var isNewBill by mutableStateOf(false) + // Null hides the pre-fill menu: it only applies to a bill started from scratch. + var prefillMode by mutableStateOf(null) + var hasPrefillPreset by mutableStateOf(false) + + // Auto-categorization: null while it is off, or for a bill that is not new. + private var autoCategorizer: ((String) -> Long?)? = null + + // Once the user picks a category themselves, the name no longer gets a say. + private var categoryChosenByUser = false + + // The category in place before an automatic one replaced it, to restore when the name stops + // matching; null while no automatic category is applied. + private var categoryBeforeAuto: Long? = null + var currencies by mutableStateOf>(emptyList()) var mainCurrencyName by mutableStateOf("") var selectedCurrencyName by mutableStateOf("") @@ -52,7 +67,8 @@ class EditBillViewModel : ViewModel() { try { val result = evalMath(sanitized) if (result != 0.0) return SupportUtil.round2(result) - } catch (_: Exception) {} + } catch (_: Exception) { + } } val parsed = parseAmount(input) ?: 0.0 return SupportUtil.round2(parsed) @@ -166,10 +182,10 @@ class EditBillViewModel : ViewModel() { fun convertCurrency(currency: DBCurrency) { selectedCurrencyName = currency.name ?: "" selectedCurrencyRate = currency.exchangeRate - + // We don't change 'amount' here anymore as per request. // It stays as the original amount. - + updateSplits() } @@ -187,7 +203,7 @@ class EditBillViewModel : ViewModel() { 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" @@ -204,21 +220,21 @@ class EditBillViewModel : ViewModel() { repeat = bill.repeat ?: DBBill.NON_REPEATED paymentModeId = bill.paymentModeId categoryId = bill.categoryId - + 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) { @@ -264,7 +280,7 @@ class EditBillViewModel : ViewModel() { val billOwerIds = bill.billOwersIds val selectedMembers = members.filter { billOwerIds.contains(it.id) } val totalWeight = selectedMembers.sumOf { it.weight } - + // Use UI amount for even split calculation val uiAmount = amountAsDouble @@ -279,6 +295,48 @@ class EditBillViewModel : ViewModel() { } } + fun applyPrefill(bill: DBBill) { + payerId = bill.payerId + categoryId = bill.categoryId + paymentModeId = bill.paymentModeId + repeat = bill.repeat ?: DBBill.NON_REPEATED + val owerIds = bill.billOwersIds.toSet() + for (member in members) { + toggleMember(member.id, member.id in owerIds) + } + // The pre-filled category is the new fallback; a name match still wins over it. + categoryBeforeAuto = null + autoCategorize() + } + + fun enableAutoCategorize(match: (String) -> Long?) { + autoCategorizer = match + } + + fun updateWhat(value: String) { + what = value + autoCategorize() + } + + fun selectCategory(id: Long) { + categoryId = id + categoryChosenByUser = true + categoryBeforeAuto = null + } + + private fun autoCategorize() { + val match = autoCategorizer ?: return + if (!isNewBill || categoryChosenByUser) return + val categoryForName = match(what) + if (categoryForName != null) { + if (categoryBeforeAuto == null) categoryBeforeAuto = categoryId + categoryId = categoryForName + } else { + categoryBeforeAuto?.let { categoryId = it } + categoryBeforeAuto = null + } + } + fun isFormValid(): Boolean { return what.isNotEmpty() && timestamp != 0L && diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt index 194f575..adb18c0 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.util.CategorySuggester class LabelBillsViewModel : ViewModel() { var billsToLabel by mutableStateOf>(emptyList()) @@ -32,26 +33,9 @@ class LabelBillsViewModel : ViewModel() { suggestedCategories = emptyList() return } - val name = bill.what.lowercase().trim() - if (name.isEmpty()) { - suggestedCategories = emptyList() - return - } - - val matches = allCategorizedBills.filter { - val otherName = it.what.lowercase().trim() - otherName == name || (name.length > 3 && otherName.contains(name)) || (otherName.length > 3 && name.contains(otherName)) - } - - val counts = matches.groupBy { it.categoryId } - .mapValues { it.value.size } - .toList() - .sortedByDescending { it.second } + suggestedCategories = CategorySuggester.rank(bill.what, allCategorizedBills) .take(2) - - suggestedCategories = counts.mapNotNull { (catId, _) -> - categoriesMap[catId] - } + .mapNotNull { (catId, _) -> categoriesMap[catId] } } fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) { diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt index 6d75c03..05945ad 100644 --- a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.Brightness2 +import androidx.compose.material.icons.filled.Category import androidx.compose.material.icons.filled.Group import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Palette @@ -95,8 +96,8 @@ fun SettingsScreen( val keyShowArchived = stringResource(R.string.pref_key_show_archived) val keyExtraFeatures = stringResource(R.string.pref_key_extra_features) val keyStatsIncludeDeactivated = stringResource(R.string.pref_key_stats_include_deactivated) + val keyAutoCategorize = stringResource(R.string.pref_key_auto_categorize) val keyFullSyncDelay = stringResource(R.string.pref_key_full_sync_delay) - val keyFillNewBillFromLast = stringResource(R.string.pref_key_fill_new_bill_from_last) val keyLastAccountSync = stringResource(R.string.pref_key_last_account_sync_timestamp) val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context) @@ -150,12 +151,12 @@ fun SettingsScreen( var extraFeatures by remember(keyExtraFeatures) { mutableStateOf(sharedPreferences.getBoolean(keyExtraFeatures, false)) } - var fillNewBillFromLast by remember(keyFillNewBillFromLast) { - mutableStateOf(sharedPreferences.getBoolean(keyFillNewBillFromLast, false)) - } var statsIncludeDeactivated by remember(keyStatsIncludeDeactivated) { mutableStateOf(sharedPreferences.getBoolean(keyStatsIncludeDeactivated, false)) } + var autoCategorize by remember(keyAutoCategorize) { + mutableStateOf(sharedPreferences.getBoolean(keyAutoCategorize, false)) + } val fullSyncDelays = SyncSettings.FULL_SYNC_DELAY_CHOICES_MINUTES val fullSyncDelayLabels = listOf( @@ -341,19 +342,6 @@ fun SettingsScreen( } ) - 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) - } - } - ) - SettingsSwitchPreference( title = stringResource(R.string.settings_stats_include_deactivated), icon = Icons.Default.Group, @@ -366,6 +354,19 @@ fun SettingsScreen( } ) + SettingsSwitchPreference( + title = stringResource(R.string.settings_auto_categorize), + summary = stringResource(R.string.settings_auto_categorize_summary), + icon = Icons.Default.Category, + checked = autoCategorize, + onCheckedChange = { + autoCategorize = it + sharedPreferences.edit { + putBoolean(keyAutoCategorize, it) + } + } + ) + SettingsSliderPreference( title = stringResource(R.string.settings_full_sync_delay), summary = stringResource(R.string.settings_full_sync_delay_summary), diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt index 69c302a..82302db 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt @@ -21,6 +21,7 @@ import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBPaymentMode import net.helcel.cowspent.model.DBProject import net.helcel.cowspent.model.ProjectType +import net.helcel.cowspent.util.BillPrefill import net.helcel.cowspent.util.CategoryUtils import net.helcel.cowspent.util.SecureStorage import net.helcel.cowspent.util.SupportUtil @@ -312,6 +313,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) : db.delete(table_projects, "$key_id = ?", projectId) } SecureStorage.removePasswordSync(context, "ProjectPassword_$id") + BillPrefill.clear(context, id) } // --- Members logic --- diff --git a/app/src/main/java/net/helcel/cowspent/util/BillPrefill.kt b/app/src/main/java/net/helcel/cowspent/util/BillPrefill.kt new file mode 100644 index 0000000..9bc1ab5 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/BillPrefill.kt @@ -0,0 +1,163 @@ +package net.helcel.cowspent.util + +import android.content.Context +import androidx.annotation.WorkerThread +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import net.helcel.cowspent.R +import net.helcel.cowspent.model.DBBill +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +/** Where a new bill takes its payer, owers, category, payment mode and repeat from. */ +enum class PrefillMode(val id: String) { + /** Start from an empty form (the project's last payer is still suggested). */ + OFF("off"), + + /** The most recent bill of the project, whoever created it and on whichever device. */ + LATEST_IN_PROJECT("project"), + + /** + * The last bill created on this device. When entering a batch of older bills, the project's + * newest bill keeps changing under you as other members sync theirs, while this one only + * follows what you typed. + */ + LATEST_ON_DEVICE("device"), + + /** Values the user saved once for the project and wants every time. */ + PRESET("preset"); + + companion object { + fun fromId(id: String?): PrefillMode? = entries.firstOrNull { it.id == id } + } +} + +/** + * The part of a bill that pre-filling carries over. Amount, description, date and comment are + * deliberately absent: those describe the one expense, not the habit. + */ +data class BillTemplate( + val payerId: Long, + val owerIds: List, + val categoryId: Long, + val paymentModeId: Long, + val paymentMode: String?, + val repeat: String +) { + fun toJson(): String = JSONObject() + .put(KEY_PAYER, payerId) + .put(KEY_OWERS, JSONArray(owerIds)) + .put(KEY_CATEGORY, categoryId) + .put(KEY_PAYMENT_MODE_ID, paymentModeId) + .put(KEY_PAYMENT_MODE, paymentMode ?: JSONObject.NULL) + .put(KEY_REPEAT, repeat) + .toString() + + companion object { + private const val KEY_PAYER = "payer" + private const val KEY_OWERS = "owers" + private const val KEY_CATEGORY = "category" + private const val KEY_PAYMENT_MODE_ID = "paymentModeId" + private const val KEY_PAYMENT_MODE = "paymentMode" + private const val KEY_REPEAT = "repeat" + + fun of(bill: DBBill) = BillTemplate( + payerId = bill.payerId, + owerIds = bill.billOwersIds, + categoryId = bill.categoryId, + paymentModeId = bill.paymentModeId, + paymentMode = bill.paymentMode, + repeat = bill.repeat ?: DBBill.NON_REPEATED + ) + + /** Null for a missing or unreadable value, so a corrupt entry acts as "nothing saved". */ + fun fromJson(json: String?): BillTemplate? { + if (json.isNullOrEmpty()) return null + return try { + val o = JSONObject(json) + val owers = o.getJSONArray(KEY_OWERS) + BillTemplate( + payerId = o.getLong(KEY_PAYER), + owerIds = List(owers.length()) { owers.getLong(it) }, + categoryId = o.optLong(KEY_CATEGORY, DBBill.CATEGORY_NONE), + paymentModeId = o.optLong(KEY_PAYMENT_MODE_ID, DBBill.PAYMODE_ID_NONE), + paymentMode = if (o.isNull(KEY_PAYMENT_MODE)) null else o.getString(KEY_PAYMENT_MODE), + repeat = o.optString(KEY_REPEAT, DBBill.NON_REPEATED) + ) + } catch (_: JSONException) { + null + } + } + } +} + +/** + * Per-project pre-fill choice and the templates behind it. Everything lives in this device's + * preferences: "latest on this device" is device-local by definition, and the mode and preset + * describe how this user enters bills, not something the other members should get. + */ +object BillPrefill { + + private fun modeKey(projectId: Long) = "prefill_mode_$projectId" + private fun deviceKey(projectId: Long) = "prefill_device_$projectId" + private fun presetKey(projectId: Long) = "prefill_preset_$projectId" + + private fun prefs(context: Context) = PreferenceManager.getDefaultSharedPreferences(context) + + fun mode(context: Context, projectId: Long): PrefillMode { + val prefs = prefs(context) + PrefillMode.fromId(prefs.getString(modeKey(projectId), null))?.let { return it } + // This used to be one global on/off switch. Projects that never picked a mode keep + // behaving the way that switch said. + val legacyOn = prefs.getBoolean(context.getString(R.string.pref_key_fill_new_bill_from_last), false) + return if (legacyOn) PrefillMode.LATEST_IN_PROJECT else PrefillMode.OFF + } + + fun setMode(context: Context, projectId: Long, mode: PrefillMode) { + prefs(context).edit { putString(modeKey(projectId), mode.id) } + } + + fun deviceTemplate(context: Context, projectId: Long): BillTemplate? = + BillTemplate.fromJson(prefs(context).getString(deviceKey(projectId), null)) + + /** Called for every bill created here, whatever the mode, so switching to it later works. */ + fun recordCreatedOnDevice(context: Context, projectId: Long, template: BillTemplate) { + prefs(context).edit { putString(deviceKey(projectId), template.toJson()) } + } + + fun preset(context: Context, projectId: Long): BillTemplate? = + BillTemplate.fromJson(prefs(context).getString(presetKey(projectId), null)) + + fun savePreset(context: Context, projectId: Long, template: BillTemplate) { + prefs(context).edit { putString(presetKey(projectId), template.toJson()) } + } + + /** Forget everything stored for a project, e.g. once it is removed from the device. */ + fun clear(context: Context, projectId: Long) { + prefs(context).edit { + remove(modeKey(projectId)) + remove(deviceKey(projectId)) + remove(presetKey(projectId)) + } + } + + /** + * The template [mode] points at, or null when there is nothing to fill from: the mode is + * off, the project has no bill yet, nothing was created on this device, or no preset was + * saved. Callers then fall back to an empty form. + */ + @WorkerThread + fun templateFor( + context: Context, + db: CowspentSQLiteOpenHelper, + projectId: Long, + mode: PrefillMode = mode(context, projectId) + ): BillTemplate? = when (mode) { + PrefillMode.OFF -> null + PrefillMode.LATEST_IN_PROJECT -> db.getLastBillOfProject(projectId)?.let(BillTemplate::of) + PrefillMode.LATEST_ON_DEVICE -> deviceTemplate(context, projectId) + PrefillMode.PRESET -> preset(context, projectId) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/util/CategorySuggester.kt b/app/src/main/java/net/helcel/cowspent/util/CategorySuggester.kt new file mode 100644 index 0000000..857e198 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/util/CategorySuggester.kt @@ -0,0 +1,64 @@ +package net.helcel.cowspent.util + +import net.helcel.cowspent.model.DBBill + +/** + * Guesses a bill's category from the names of bills that already have one. + * + * The bill-labelling screen shows the top guesses and lets the user pick; the bill editor can + * apply one by itself, so it asks [confidentMatch] instead, which only answers when the history + * clearly agrees. + */ +object CategorySuggester { + + /** Below this length a name matches too many others by containment ("a", "bus", ...). */ + private const val MIN_CONTAINMENT_LENGTH = 4 + + /** Share of the matching bills the leading category needs before it is applied unasked. */ + private const val CONFIDENT_SHARE = 0.75 + + /** Matches by containment are looser, so one of them alone is not enough. */ + private const val MIN_LOOSE_MATCHES = 2 + + private fun normalize(name: String) = name.lowercase().trim() + + private fun isExactMatch(name: String, other: String) = other == name + + private fun isLooseMatch(name: String, other: String) = + (name.length >= MIN_CONTAINMENT_LENGTH && other.contains(name)) || + (other.length >= MIN_CONTAINMENT_LENGTH && name.contains(other)) + + fun rank(what: String, categorizedBills: List): List> { + val name = normalize(what) + if (name.isEmpty()) return emptyList() + return categorizedBills + .filter { val other = normalize(it.what); isExactMatch(name, other) || isLooseMatch(name, other) } + .groupingBy { it.categoryId } + .eachCount() + .toList() + .sortedByDescending { it.second } + } + + fun confidentMatch( + what: String, + categorizedBills: List, + allowedCategoryIds: Set + ): Long? { + val name = normalize(what) + if (name.isEmpty()) return null + val candidates = categorizedBills.filter { it.categoryId in allowedCategoryIds } + + val exact = candidates.filter { isExactMatch(name, normalize(it.what)) } + if (exact.isNotEmpty()) return dominant(exact, minCount = 1) + + val loose = candidates.filter { isLooseMatch(name, normalize(it.what)) } + return dominant(loose, minCount = MIN_LOOSE_MATCHES) + } + + private fun dominant(bills: List, minCount: Int): Long? { + if (bills.size < minCount) return null + val (categoryId, count) = bills.groupingBy { it.categoryId }.eachCount().maxByOrNull { it.value } + ?: return null + return if (count >= minCount && count >= bills.size * CONFIDENT_SHARE) categoryId else null + } +} diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index cb4b76a..f7d9eca 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -138,8 +138,6 @@ Eigene Farbe Farbauswahl Archivierte Projekte anzeigen - Aus letzter Rechnung vorausfüllen - Zahler, Kategorie, Zahlungsart und Beteiligte aus der zuletzt erstellten Rechnung des Projekts übernehmen. 1 Stunde 1 Tag WARNUNG: \"http\" ist unsicher. Verwende \"https\". diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index d53f914..5898727 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -137,8 +137,6 @@ Color personalizado Selección de color Mostrar los proyectos archivados - Rellenar desde la última factura - Reutilizar el pagador, la categoría, el modo y los participantes de la última factura creada en el proyecto. 1 hora 1 día ADVERTENCIA: \"http\" no es seguro. Usa \"https\". diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 1d6c59e..450ce77 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -138,8 +138,6 @@ Couleur personnalisée Sélection de la couleur Afficher les projets archivés - Pré-remplir depuis la dernière facture - Reprendre le payeur, la catégorie, le mode et les participants de la dernière facture créée dans le projet. 1 heure 1 jour AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\". diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 48f7908..6edfe8c 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -137,8 +137,6 @@ Пользовательский цвет Выбор цвета Показывать заархивированные проекты - Автозаполнение на основе последнего расхода - Предварительное заполнение полей "Плательщик", "Категория", "Способ оплаты" и "Для кого" на основе последнего расхода, созданного в проекте. 1 час 1 день ПРЕДУПРЕЖДЕНИЕ: "http" небезопасен. Используйте "https". diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3dd792e..70862f0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -144,8 +144,6 @@ Custom color Color Selection Show archived projects - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. 1 hour 1 day WARNING: "http" is unsafe. Use "https". diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc8f2a8..b9e9e0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,12 +146,20 @@ Color Selection Show archived projects Extra Features - Experimental and advanced settings. Use at your own risk. - Auto-fill from last bill - Pre-fill payer, category, mode and owers from the last bill created in the project. + Experimental and advanced options. + Pre-fill + Nothing + Last from project + Last from device + Saved preset + None saved yet + Save current as preset + Preset saved for this project Include deactivated members in stats + Auto-categorize new bills + Pick the category from similar past bills. Full sync delay - How long a pull-to-refresh waits between two full syncs. In between, it only fetches what changed, which is much faster. + Time between full syncs. Other refreshes only fetch changes. Always 1 hour 1 day @@ -178,6 +186,7 @@ showArchived betaFeatures statsIncludeDeactivated + autoCategorizeNewBills fullSyncDelayMinutes lastAccountSyncTimestamp fillNewBillFromLast