Compare commits
1
Commits
main
..
99d031551d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99d031551d |
No files matched your search
+15
-21
@@ -8,11 +8,11 @@ env:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- "**"
|
||||
- '**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
|
||||
echo "${{ secrets.RELEASE_KEY}}" > key.asc
|
||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
|
||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
|
||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
|
||||
|
||||
- name: Generate Dynamic Release Notes
|
||||
run: |
|
||||
mkdir -p tmp/whatsnew
|
||||
echo "Various improvements for you to experience..." > tmp/whatsnew/whatsnew-en-US
|
||||
|
||||
|
||||
- name: create and checkout branch
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
@@ -67,8 +67,9 @@ jobs:
|
||||
else
|
||||
VERSION_NAME="${VERSION_BASE}"
|
||||
fi
|
||||
printf '%s\n%s\n' "$VERSION_CODE" "$VERSION_NAME" > version.txt
|
||||
./gradlew bundleSignedRelease assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||
# ./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||
# ./gradlew bundleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -77,33 +78,26 @@ jobs:
|
||||
compression-level: 0
|
||||
archive: false
|
||||
|
||||
- name: Determine prerelease
|
||||
id: prerelease
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
if [[ "$GITHUB_REF_NAME" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then
|
||||
echo "value=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "value=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||
version.txt
|
||||
prerelease: ${{ steps.prerelease.outputs.value }}
|
||||
prerelease: |
|
||||
if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+\.[0-9]+[a-z]+$ ]]; then
|
||||
echo "false"
|
||||
else
|
||||
echo "true"
|
||||
fi
|
||||
|
||||
- name: Upload to Google Play
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
if: github.event_name == 'push'
|
||||
continue-on-error: true
|
||||
# if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: net.helcel.cowspent
|
||||
releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab
|
||||
tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'internal' }}
|
||||
status: completed
|
||||
whatsNewDirectory: tmp/whatsnew
|
||||
whatsNewDirectory: tmp/whatsnew
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '9.5.1'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.20'
|
||||
id 'com.android.application' version '9.3.2'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.21'
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.20'
|
||||
id 'jacoco'
|
||||
}
|
||||
@@ -129,7 +129,7 @@ dependencies {
|
||||
implementation 'androidx.camera:camera-camera2:1.6.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.6.2'
|
||||
implementation 'androidx.camera:camera-view:1.6.2'
|
||||
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.5'
|
||||
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.4'
|
||||
implementation 'com.opencsv:opencsv:5.12.0'
|
||||
|
||||
implementation platform('androidx.compose:compose-bom:2026.09.00')
|
||||
@@ -142,7 +142,7 @@ dependencies {
|
||||
implementation 'androidx.activity:activity-ktx:1.13.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0'
|
||||
implementation "androidx.datastore:datastore-preferences:1.3.0"
|
||||
implementation "androidx.datastore:datastore-preferences:1.2.1"
|
||||
implementation "com.google.crypto.tink:tink-android:1.23.0"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -28,10 +27,6 @@ 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
|
||||
@@ -45,9 +40,6 @@ 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)
|
||||
@@ -103,14 +95,8 @@ 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,
|
||||
onPrefillModeSelected = { onPrefillModeSelected(it) },
|
||||
onSavePrefillPreset = { onSavePrefillPreset() },
|
||||
onDuplicate = if (!viewModel.isNewBill) { { duplicateCurrentBill() } } else null,
|
||||
onDelete = if (!viewModel.isNewBill) { { deleteBillAsked() } } else null,
|
||||
accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN
|
||||
)
|
||||
}
|
||||
@@ -147,7 +133,7 @@ class EditBillActivity : AppCompatActivity() {
|
||||
first.timestamp, first.what, first.state, first.repeat,
|
||||
first.paymentMode, first.categoryId, first.comment, first.paymentModeId
|
||||
)
|
||||
|
||||
|
||||
val splits = mutableMapOf<Long, Double>()
|
||||
for (b in sourceBills) {
|
||||
val owersCount = b.billOwers.size
|
||||
@@ -166,13 +152,25 @@ class EditBillActivity : AppCompatActivity() {
|
||||
val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0)
|
||||
val timeNowSeconds = System.currentTimeMillis() / 1000
|
||||
if (billIdToDuplicate == 0L) {
|
||||
val mode = BillPrefill.mode(applicationContext, projectId)
|
||||
bill = newBill(
|
||||
projectId,
|
||||
timeNowSeconds,
|
||||
BillPrefill.templateFor(applicationContext, db, projectId, mode)
|
||||
)
|
||||
prefillMode = mode
|
||||
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
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val btd = db.getBill(billIdToDuplicate)!!
|
||||
bill = DBBill(
|
||||
@@ -195,107 +193,23 @@ 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) {
|
||||
@@ -337,7 +251,7 @@ class EditBillActivity : AppCompatActivity() {
|
||||
calendar.timeInMillis = System.currentTimeMillis()
|
||||
viewModel.timestamp = calendar.timeInMillis / 1000
|
||||
viewModel.isNewBill = true
|
||||
|
||||
|
||||
showToast(this, "Duplicating bill...")
|
||||
}
|
||||
|
||||
@@ -368,18 +282,8 @@ 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().also { id ->
|
||||
if (createdHere && id != 0L) {
|
||||
BillPrefill.recordCreatedOnDevice(applicationContext, bill.projectId, template)
|
||||
}
|
||||
}
|
||||
}
|
||||
val savedBillId = withContext(Dispatchers.IO) { saveBill() }
|
||||
val data = Intent()
|
||||
data.putExtra(MainConstants.SAVED_BILL_ID, savedBillId)
|
||||
setResult(RESULT_OK, data)
|
||||
@@ -457,7 +361,7 @@ class EditBillActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (splits.isEmpty()) return@withContext 0L
|
||||
|
||||
|
||||
val finalComment = viewModel.getFinalComment()
|
||||
val splitEntries = splits.entries.toList()
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ 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
|
||||
@@ -67,8 +66,6 @@ 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
|
||||
@@ -105,14 +102,6 @@ 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)
|
||||
}
|
||||
@@ -229,65 +218,6 @@ 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
|
||||
@@ -452,7 +382,7 @@ fun BillBasicInfoSection(
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.what,
|
||||
onValueChange = { viewModel.updateWhat(it) },
|
||||
onValueChange = { viewModel.what = it },
|
||||
enabled = canEdit,
|
||||
placeholder = { Text(stringResource(R.string.label_what)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -756,7 +686,7 @@ fun BillAdditionalDetailsSection(
|
||||
},
|
||||
content = {
|
||||
DropdownMenuItem(onClick = {
|
||||
viewModel.selectCategory(DBBill.CATEGORY_NONE)
|
||||
viewModel.categoryId = 0
|
||||
categoryExpanded = false
|
||||
}) {
|
||||
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
|
||||
@@ -765,7 +695,7 @@ fun BillAdditionalDetailsSection(
|
||||
}
|
||||
|
||||
DropdownMenuItem(onClick = {
|
||||
viewModel.selectCategory(DBBill.CATEGORY_REIMBURSEMENT)
|
||||
viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT
|
||||
categoryExpanded = false
|
||||
}) {
|
||||
Text(text = "\uD83D\uDCB0", fontSize = 20.sp)
|
||||
@@ -775,7 +705,7 @@ fun BillAdditionalDetailsSection(
|
||||
|
||||
categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category ->
|
||||
DropdownMenuItem(onClick = {
|
||||
viewModel.selectCategory(category.id)
|
||||
viewModel.categoryId = category.id
|
||||
categoryExpanded = false
|
||||
}) {
|
||||
Text(text = category.icon, fontSize = 20.sp)
|
||||
|
||||
@@ -13,7 +13,6 @@ 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
|
||||
|
||||
@@ -32,20 +31,6 @@ 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<PrefillMode?>(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<List<DBCurrency>>(emptyList())
|
||||
var mainCurrencyName by mutableStateOf("")
|
||||
var selectedCurrencyName by mutableStateOf("")
|
||||
@@ -67,8 +52,7 @@ 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)
|
||||
@@ -182,10 +166,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()
|
||||
}
|
||||
|
||||
@@ -203,7 +187,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"
|
||||
@@ -220,21 +204,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) {
|
||||
@@ -280,7 +264,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
|
||||
|
||||
@@ -295,48 +279,6 @@ 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 &&
|
||||
|
||||
@@ -8,7 +8,6 @@ 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<List<DBBill>>(emptyList())
|
||||
@@ -33,9 +32,26 @@ class LabelBillsViewModel : ViewModel() {
|
||||
suggestedCategories = emptyList()
|
||||
return
|
||||
}
|
||||
suggestedCategories = CategorySuggester.rank(bill.what, allCategorizedBills)
|
||||
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 }
|
||||
.take(2)
|
||||
.mapNotNull { (catId, _) -> categoriesMap[catId] }
|
||||
|
||||
suggestedCategories = counts.mapNotNull { (catId, _) ->
|
||||
categoriesMap[catId]
|
||||
}
|
||||
}
|
||||
|
||||
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) {
|
||||
|
||||
@@ -32,7 +32,6 @@ 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
|
||||
@@ -96,8 +95,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)
|
||||
@@ -151,12 +150,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(
|
||||
@@ -342,6 +341,19 @@ 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,
|
||||
@@ -354,19 +366,6 @@ 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),
|
||||
|
||||
@@ -21,7 +21,6 @@ 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
|
||||
@@ -313,7 +312,6 @@ 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 ---
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
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<Long>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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<DBBill>): List<Pair<Long, Int>> {
|
||||
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<DBBill>,
|
||||
allowedCategoryIds: Set<Long>
|
||||
): 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<DBBill>, 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
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
<string name="settings_color_custom">Eigene Farbe</string>
|
||||
<string name="settings_color_mode">Farbauswahl</string>
|
||||
<string name="settings_show_archived">Archivierte Projekte anzeigen</string>
|
||||
<string name="settings_fill_new_bill_from_last">Aus letzter Rechnung vorausfüllen</string>
|
||||
<string name="settings_fill_new_bill_from_last_summary">Zahler, Kategorie, Zahlungsart und Beteiligte aus der zuletzt erstellten Rechnung des Projekts übernehmen.</string>
|
||||
<string name="pref_value_sync_1h">1 Stunde</string>
|
||||
<string name="pref_value_sync_1d">1 Tag</string>
|
||||
<string name="settings_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwende \"https\".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
<string name="settings_color_custom">Color personalizado</string>
|
||||
<string name="settings_color_mode">Selección de color</string>
|
||||
<string name="settings_show_archived">Mostrar los proyectos archivados</string>
|
||||
<string name="settings_fill_new_bill_from_last">Rellenar desde la última factura</string>
|
||||
<string name="settings_fill_new_bill_from_last_summary">Reutilizar el pagador, la categoría, el modo y los participantes de la última factura creada en el proyecto.</string>
|
||||
<string name="pref_value_sync_1h">1 hora</string>
|
||||
<string name="pref_value_sync_1d">1 día</string>
|
||||
<string name="settings_url_warn_http">ADVERTENCIA: \"http\" no es seguro. Usa \"https\".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
<string name="settings_color_custom">Couleur personnalisée</string>
|
||||
<string name="settings_color_mode">Sélection de la couleur</string>
|
||||
<string name="settings_show_archived">Afficher les projets archivés</string>
|
||||
<string name="settings_fill_new_bill_from_last">Pré-remplir depuis la dernière facture</string>
|
||||
<string name="settings_fill_new_bill_from_last_summary">Reprendre le payeur, la catégorie, le mode et les participants de la dernière facture créée dans le projet.</string>
|
||||
<string name="pref_value_sync_1h">1 heure</string>
|
||||
<string name="pref_value_sync_1d">1 jour</string>
|
||||
<string name="settings_url_warn_http">AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
<string name="settings_color_custom">Пользовательский цвет</string>
|
||||
<string name="settings_color_mode">Выбор цвета</string>
|
||||
<string name="settings_show_archived">Показывать заархивированные проекты</string>
|
||||
<string name="settings_fill_new_bill_from_last">Автозаполнение на основе последнего расхода</string>
|
||||
<string name="settings_fill_new_bill_from_last_summary">Предварительное заполнение полей "Плательщик", "Категория", "Способ оплаты" и "Для кого" на основе последнего расхода, созданного в проекте.</string>
|
||||
<string name="pref_value_sync_1h">1 час</string>
|
||||
<string name="pref_value_sync_1d">1 день</string>
|
||||
<string name="settings_url_warn_http">ПРЕДУПРЕЖДЕНИЕ: "http" небезопасен. Используйте "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</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="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
|
||||
@@ -146,20 +146,12 @@
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</string>
|
||||
<string name="settings_extra_features">Extra Features</string>
|
||||
<string name="settings_extra_features_summary">Experimental and advanced options.</string>
|
||||
<string name="prefill_button">Pre-fill</string>
|
||||
<string name="prefill_mode_off">Nothing</string>
|
||||
<string name="prefill_mode_latest_project">Last from project</string>
|
||||
<string name="prefill_mode_latest_device">Last from device</string>
|
||||
<string name="prefill_mode_preset">Saved preset</string>
|
||||
<string name="prefill_mode_preset_empty">None saved yet</string>
|
||||
<string name="prefill_save_preset">Save current as preset</string>
|
||||
<string name="prefill_preset_saved">Preset saved for this project</string>
|
||||
<string name="settings_extra_features_summary">Experimental and advanced settings. 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_stats_include_deactivated">Include deactivated members in stats</string>
|
||||
<string name="settings_auto_categorize">Auto-categorize new bills</string>
|
||||
<string name="settings_auto_categorize_summary">Pick the category from similar past bills.</string>
|
||||
<string name="settings_full_sync_delay">Full sync delay</string>
|
||||
<string name="settings_full_sync_delay_summary">Time between full syncs. Other refreshes only fetch changes.</string>
|
||||
<string name="settings_full_sync_delay_summary">How long a pull-to-refresh waits between two full syncs. In between, it only fetches what changed, which is much faster.</string>
|
||||
<string name="pref_value_sync_always">Always</string>
|
||||
<string name="pref_value_sync_1h">1 hour</string>
|
||||
<string name="pref_value_sync_1d">1 day</string>
|
||||
@@ -186,7 +178,6 @@
|
||||
<string name="pref_key_show_archived" translatable="false">showArchived</string>
|
||||
<string name="pref_key_extra_features" translatable="false">betaFeatures</string>
|
||||
<string name="pref_key_stats_include_deactivated" translatable="false">statsIncludeDeactivated</string>
|
||||
<string name="pref_key_auto_categorize" translatable="false">autoCategorizeNewBills</string>
|
||||
<string name="pref_key_full_sync_delay" translatable="false">fullSyncDelayMinutes</string>
|
||||
<string name="pref_key_last_account_sync_timestamp" translatable="false">lastAccountSyncTimestamp</string>
|
||||
<string name="pref_key_fill_new_bill_from_last" translatable="false">fillNewBillFromLast</string>
|
||||
|
||||
Reference in new issue
Block a user