Compare commits

..
26 Commits
Author SHA1 Message Date
sora c21f0c2d7a Undo java bump breaking build 2026-11-14 13:47:24 +01:00
sora d8fd4aaded build with version file 2026-11-14 13:39:31 +01:00
sora a5c825c8e0 better prefill, autocategorize 2026-11-14 13:34:43 +01:00
sora 206223bff5 Merge pull request 'Update plugin com.android.application to v9.5.1' (#7) from renovate/com.android.application-9.x into main
Reviewed-on: #7
2026-11-14 12:24:53 +01:00
sora 3deaff97fb Merge pull request 'Update dependency androidx.datastore:datastore-preferences to v1.3.0' (#25) from renovate/androidx.datastore-datastore-preferences-1.x into main
Reviewed-on: #25
2026-11-14 12:24:14 +01:00
sora 9e6ba6e1b7 Merge pull request 'Update dependency java-jdk to v27' (#26) from renovate/java-jdk-27.x into main
Reviewed-on: #26
2026-11-14 12:23:59 +01:00
sora af72449037 Merge pull request 'Update dependency com.github.nextcloud:Android-SingleSignOn to v1.3.5' (#27) from renovate/com.github.nextcloud-android-singlesignon-1.x into main
Reviewed-on: #27
2026-11-14 12:23:47 +01:00
Renovate Bot 211f751c0e Update plugin com.android.application to v9.5.1 2026-11-06 02:01:21 +00:00
Renovate Bot bad6d59c5c Update dependency com.github.nextcloud:Android-SingleSignOn to v1.3.5 2026-10-29 02:01:11 +00:00
Renovate Bot 7ecd10abf0 Update dependency java-jdk to v27 2026-10-23 02:02:12 +00:00
Renovate Bot 04c7deb066 Update dependency androidx.datastore:datastore-preferences to v1.3.0 2026-10-22 02:01:42 +00:00
sora c2da40ba4d Update .github/workflows/build.yml 2026-10-03 19:38:47 +02:00
sora 6e45507b22 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.20' (#15) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main
Reviewed-on: #15
2026-10-03 19:33:49 +02:00
sora 99d05680c5 Merge pull request 'Update dependency androidx.compose.runtime:runtime to v1.12.1' (#17) from renovate/androidx.compose.runtime-runtime-1.x into main
Reviewed-on: #17
2026-10-03 19:33:35 +02:00
Renovate Bot d89d99ceff Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.20 2026-10-01 02:01:46 +00:00
Renovate Bot 40be1b79f2 Update dependency androidx.compose.runtime:runtime to v1.12.1 2026-10-01 02:01:44 +00:00
sora e807d6fa66 Merge pull request 'Update dependency androidx.work:work-runtime-ktx to v2.12.0' (#20) from renovate/androidx.work-work-runtime-ktx-2.x into main
Reviewed-on: #20
2026-09-30 23:03:02 +02:00
sora 8ae8df6ef8 Merge pull request 'Update dependency org.robolectric:robolectric to v4.17' (#19) from renovate/org.robolectric-robolectric-4.x into main
Reviewed-on: #19
2026-09-30 23:02:53 +02:00
sora 791f3572b6 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.09.00' (#18) from renovate/androidx.compose-compose-bom-2026.x into main
Reviewed-on: #18
2026-09-30 23:02:34 +02:00
sora ce8053ff2c Merge pull request 'Update dependency androidx.compose.foundation:foundation to v1.12.1' (#16) from renovate/androidx.compose.foundation-foundation-1.x into main
Reviewed-on: #16
2026-09-30 23:02:15 +02:00
sora f5f4c083f0 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.20' (#14) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main
Reviewed-on: #14
2026-09-30 23:01:39 +02:00
Renovate Bot 1f56757d30 Update dependency androidx.work:work-runtime-ktx to v2.12.0 2026-09-24 02:02:58 +00:00
Renovate Bot 73dc64a688 Update dependency org.robolectric:robolectric to v4.17 2026-09-11 02:01:48 +00:00
Renovate Bot 3b22037496 Update dependency androidx.compose:compose-bom to v2026.09.00 2026-09-11 02:01:46 +00:00
Renovate Bot 206c756ad7 Update dependency androidx.compose.foundation:foundation to v1.12.1 2026-09-10 02:01:21 +00:00
Renovate Bot a66d046b3f Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.20 2026-09-08 02:01:15 +00:00
40 changed files with 540 additions and 146 deletions

No files matched your search

+16 -11
View File
@@ -10,7 +10,7 @@ on:
push: push:
branches: [main] branches: [main]
tags: tags:
- '**' - "**"
pull_request: pull_request:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
@@ -67,9 +67,8 @@ jobs:
else else
VERSION_NAME="${VERSION_BASE}" VERSION_NAME="${VERSION_BASE}"
fi fi
printf '%s\n%s\n' "$VERSION_CODE" "$VERSION_NAME" > version.txt
./gradlew bundleSignedRelease assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME ./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 - name: Upload APK
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
@@ -78,26 +77,32 @@ jobs:
compression-level: 0 compression-level: 0
archive: false archive: false
- name: Determine prerelease
id: prerelease
if: startsWith(github.ref, 'refs/tags/')
run: |
if [[ "$GITHUB_REF_NAME" =~ ^[0-9]+\.[0-9]+[a-z]+$ ]]; then
echo "value=false" >> "$GITHUB_OUTPUT"
else
echo "value=true" >> "$GITHUB_OUTPUT"
fi
- name: Release - name: Release
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
app/build/outputs/apk/signedRelease/app-signedRelease.apk app/build/outputs/apk/signedRelease/app-signedRelease.apk
prerelease: | version.txt
if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+\.[0-9]+[a-z]+$ ]]; then prerelease: ${{ steps.prerelease.outputs.value }}
echo "false"
else
echo "true"
fi
- name: Upload to Google Play - name: Upload to Google Play
uses: r0adkll/upload-google-play@v1 uses: r0adkll/upload-google-play@v1
if: startsWith(github.ref, 'refs/tags/') if: github.event_name == 'push'
with: with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: net.helcel.cowspent packageName: net.helcel.cowspent
releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab
tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'alpha' }} tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'internal' }}
status: completed status: completed
whatsNewDirectory: tmp/whatsnew whatsNewDirectory: tmp/whatsnew
+11 -11
View File
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' version '9.3.2' id 'com.android.application' version '9.5.1'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10' id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.20'
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10' id 'org.jetbrains.kotlin.plugin.compose' version '2.4.20'
id 'jacoco' id 'jacoco'
} }
@@ -118,21 +118,21 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.compose.foundation:foundation:1.12.0' implementation 'androidx.compose.foundation:foundation:1.12.1'
implementation 'androidx.compose.runtime:runtime:1.12.0' implementation 'androidx.compose.runtime:runtime:1.12.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.work:work-runtime-ktx:2.11.2' implementation 'androidx.work:work-runtime-ktx:2.12.0'
implementation 'com.google.zxing:core:3.5.4' implementation 'com.google.zxing:core:3.5.4'
implementation 'androidx.camera:camera-camera2:1.6.2' implementation 'androidx.camera:camera-camera2:1.6.2'
implementation 'androidx.camera:camera-lifecycle:1.6.2' implementation 'androidx.camera:camera-lifecycle:1.6.2'
implementation 'androidx.camera:camera-view:1.6.2' implementation 'androidx.camera:camera-view:1.6.2'
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.4' implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.5'
implementation 'com.opencsv:opencsv:5.12.0' implementation 'com.opencsv:opencsv:5.12.0'
implementation platform('androidx.compose:compose-bom:2026.08.00') implementation platform('androidx.compose:compose-bom:2026.09.00')
implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material:material' implementation 'androidx.compose.material:material'
implementation 'androidx.compose.material:material-icons-extended' implementation 'androidx.compose.material:material-icons-extended'
@@ -142,17 +142,17 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.13.0' implementation 'androidx.activity:activity-ktx:1.13.0'
implementation 'androidx.security:security-crypto:1.1.0' implementation 'androidx.security:security-crypto:1.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0'
implementation "androidx.datastore:datastore-preferences:1.2.1" implementation "androidx.datastore:datastore-preferences:1.3.0"
implementation "com.google.crypto.tink:tink-android:1.23.0" implementation "com.google.crypto.tink:tink-android:1.23.0"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.14.11' testImplementation 'io.mockk:mockk:1.14.11'
testImplementation 'org.robolectric:robolectric:4.16.1' testImplementation 'org.robolectric:robolectric:4.17'
testImplementation 'androidx.test:core:1.7.0' testImplementation 'androidx.test:core:1.7.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0'
testImplementation 'androidx.compose.ui:ui-test-junit4' testImplementation 'androidx.compose.ui:ui-test-junit4'
androidTestImplementation platform('androidx.compose:compose-bom:2026.08.00') androidTestImplementation platform('androidx.compose:compose-bom:2026.09.00')
androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4' androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
@@ -9,6 +9,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -27,6 +28,10 @@ import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillParser 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 net.helcel.cowspent.util.SupportUtil
import java.text.ParseException import java.text.ParseException
import java.time.ZoneId import java.time.ZoneId
@@ -40,6 +45,9 @@ class EditBillActivity : AppCompatActivity() {
private var projectType: ProjectType = ProjectType.LOCAL private var projectType: ProjectType = ProjectType.LOCAL
private val calendar = Calendar.getInstance() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -95,8 +103,14 @@ class EditBillActivity : AppCompatActivity() {
val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java) val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java)
scanQRCodeLauncher.launch(createIntent) scanQRCodeLauncher.launch(createIntent)
}, },
onDuplicate = if (!viewModel.isNewBill) { { duplicateCurrentBill() } } else null, onDuplicate = if (!viewModel.isNewBill) {
onDelete = if (!viewModel.isNewBill) { { deleteBillAsked() } } else null, { 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 accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN
) )
} }
@@ -152,25 +166,13 @@ class EditBillActivity : AppCompatActivity() {
val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0) val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0)
val timeNowSeconds = System.currentTimeMillis() / 1000 val timeNowSeconds = System.currentTimeMillis() / 1000
if (billIdToDuplicate == 0L) { if (billIdToDuplicate == 0L) {
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) val mode = BillPrefill.mode(applicationContext, projectId)
val fillFromLast = preferences.getBoolean(getString(R.string.pref_key_fill_new_bill_from_last), false) bill = newBill(
val lastBill = if (fillFromLast) db.getLastBillOfProject(projectId) else null projectId,
timeNowSeconds,
if (lastBill != null) { BillPrefill.templateFor(applicationContext, db, projectId, mode)
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) } prefillMode = mode
} else {
val project = db.getProject(projectId)
bill = DBBill(
0, 0, projectId, project?.lastPayerId ?: 0, 0.0, timeNowSeconds,
"", DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE
)
}
} else { } else {
val btd = db.getBill(billIdToDuplicate)!! val btd = db.getBill(billIdToDuplicate)!!
bill = DBBill( bill = DBBill(
@@ -201,15 +203,99 @@ class EditBillActivity : AppCompatActivity() {
db.ensureDefaultLabels(bill.projectId, project.type) 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) { withContext(Dispatchers.Main) {
viewModel.currencies = currencies viewModel.currencies = currencies
viewModel.mainCurrencyName = project?.currencyName ?: "" viewModel.mainCurrencyName = project?.currencyName ?: ""
viewModel.isNewBill = (bill.id == 0L) viewModel.isNewBill = (bill.id == 0L)
viewModel.prefillMode = prefillMode
viewModel.hasPrefillPreset = hasPreset
viewModel.initFromBill(bill, members, customSplits) 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 = private val scanQRCodeLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK && result.data != null) { if (result.resultCode == RESULT_OK && result.data != null) {
@@ -282,8 +368,18 @@ class EditBillActivity : AppCompatActivity() {
if (validationError != null) { if (validationError != null) {
showToast(this, validationError) showToast(this, validationError)
} else { } 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 { 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() val data = Intent()
data.putExtra(MainConstants.SAVED_BILL_ID, savedBillId) data.putExtra(MainConstants.SAVED_BILL_ID, savedBillId)
setResult(RESULT_OK, data) setResult(RESULT_OK, data)
@@ -49,6 +49,7 @@ import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.* import net.helcel.cowspent.android.helper.*
import net.helcel.cowspent.model.* import net.helcel.cowspent.model.*
import net.helcel.cowspent.util.CategoryUtils import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.PrefillMode
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import java.util.Date import java.util.Date
import kotlin.math.abs import kotlin.math.abs
@@ -66,6 +67,8 @@ fun EditBillScreen(
onScan: () -> Unit, onScan: () -> Unit,
onDuplicate: (() -> Unit)? = null, onDuplicate: (() -> Unit)? = null,
onDelete: (() -> Unit)? = null, onDelete: (() -> Unit)? = null,
onPrefillModeSelected: (PrefillMode) -> Unit = {},
onSavePrefillPreset: () -> Unit = {},
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN
) { ) {
val canEdit = accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN || accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT val canEdit = accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN || accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT
@@ -102,6 +105,14 @@ fun EditBillScreen(
}, },
actions = { actions = {
if (canEdit) { if (canEdit) {
viewModel.prefillMode?.let { mode ->
PrefillMenu(
mode = mode,
hasPreset = viewModel.hasPrefillPreset,
onModeSelected = onPrefillModeSelected,
onSavePreset = onSavePrefillPreset
)
}
IconButton(onClick = onScan) { IconButton(onClick = onScan) {
Icon(Icons.Default.QrCodeScanner, contentDescription = null) Icon(Icons.Default.QrCodeScanner, contentDescription = null)
} }
@@ -218,6 +229,65 @@ fun EditBillScreen(
private val MathOperatorRowHeight = 54.dp 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) private data class KeyboardColors(val tray: Color, val edge: Color, val key: Color, val label: Color)
@Composable @Composable
@@ -382,7 +452,7 @@ fun BillBasicInfoSection(
OutlinedTextField( OutlinedTextField(
value = viewModel.what, value = viewModel.what,
onValueChange = { viewModel.what = it }, onValueChange = { viewModel.updateWhat(it) },
enabled = canEdit, enabled = canEdit,
placeholder = { Text(stringResource(R.string.label_what)) }, placeholder = { Text(stringResource(R.string.label_what)) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -686,7 +756,7 @@ fun BillAdditionalDetailsSection(
}, },
content = { content = {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.categoryId = 0 viewModel.selectCategory(DBBill.CATEGORY_NONE)
categoryExpanded = false categoryExpanded = false
}) { }) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null) Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
@@ -695,7 +765,7 @@ fun BillAdditionalDetailsSection(
} }
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT viewModel.selectCategory(DBBill.CATEGORY_REIMBURSEMENT)
categoryExpanded = false categoryExpanded = false
}) { }) {
Text(text = "\uD83D\uDCB0", fontSize = 20.sp) Text(text = "\uD83D\uDCB0", fontSize = 20.sp)
@@ -705,7 +775,7 @@ fun BillAdditionalDetailsSection(
categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category -> categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category ->
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.categoryId = category.id viewModel.selectCategory(category.id)
categoryExpanded = false categoryExpanded = false
}) { }) {
Text(text = category.icon, fontSize = 20.sp) Text(text = category.icon, fontSize = 20.sp)
@@ -13,6 +13,7 @@ import net.helcel.cowspent.android.helper.parseAmount
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCurrency import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.PrefillMode
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath import net.helcel.cowspent.util.evalMath
@@ -31,6 +32,20 @@ class EditBillViewModel : ViewModel() {
var categoryId by mutableLongStateOf(0L) var categoryId by mutableLongStateOf(0L)
var isNewBill by mutableStateOf(false) 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 currencies by mutableStateOf<List<DBCurrency>>(emptyList())
var mainCurrencyName by mutableStateOf("") var mainCurrencyName by mutableStateOf("")
var selectedCurrencyName by mutableStateOf("") var selectedCurrencyName by mutableStateOf("")
@@ -52,7 +67,8 @@ class EditBillViewModel : ViewModel() {
try { try {
val result = evalMath(sanitized) val result = evalMath(sanitized)
if (result != 0.0) return SupportUtil.round2(result) if (result != 0.0) return SupportUtil.round2(result)
} catch (_: Exception) {} } catch (_: Exception) {
}
} }
val parsed = parseAmount(input) ?: 0.0 val parsed = parseAmount(input) ?: 0.0
return SupportUtil.round2(parsed) return SupportUtil.round2(parsed)
@@ -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 { fun isFormValid(): Boolean {
return what.isNotEmpty() && return what.isNotEmpty() &&
timestamp != 0L && timestamp != 0L &&
@@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategorySuggester
class LabelBillsViewModel : ViewModel() { class LabelBillsViewModel : ViewModel() {
var billsToLabel by mutableStateOf<List<DBBill>>(emptyList()) var billsToLabel by mutableStateOf<List<DBBill>>(emptyList())
@@ -32,26 +33,9 @@ class LabelBillsViewModel : ViewModel() {
suggestedCategories = emptyList() suggestedCategories = emptyList()
return return
} }
val name = bill.what.lowercase().trim() suggestedCategories = CategorySuggester.rank(bill.what, allCategorizedBills)
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) .take(2)
.mapNotNull { (catId, _) -> categoriesMap[catId] }
suggestedCategories = counts.mapNotNull { (catId, _) ->
categoriesMap[catId]
}
} }
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) { fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) {
@@ -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.AccountCircle
import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Brightness2 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.Group
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Palette
@@ -95,8 +96,8 @@ fun SettingsScreen(
val keyShowArchived = stringResource(R.string.pref_key_show_archived) val keyShowArchived = stringResource(R.string.pref_key_show_archived)
val keyExtraFeatures = stringResource(R.string.pref_key_extra_features) val keyExtraFeatures = stringResource(R.string.pref_key_extra_features)
val keyStatsIncludeDeactivated = stringResource(R.string.pref_key_stats_include_deactivated) 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 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 keyLastAccountSync = stringResource(R.string.pref_key_last_account_sync_timestamp)
val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context) val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context)
@@ -150,12 +151,12 @@ fun SettingsScreen(
var extraFeatures by remember(keyExtraFeatures) { var extraFeatures by remember(keyExtraFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyExtraFeatures, false)) mutableStateOf(sharedPreferences.getBoolean(keyExtraFeatures, false))
} }
var fillNewBillFromLast by remember(keyFillNewBillFromLast) {
mutableStateOf(sharedPreferences.getBoolean(keyFillNewBillFromLast, false))
}
var statsIncludeDeactivated by remember(keyStatsIncludeDeactivated) { var statsIncludeDeactivated by remember(keyStatsIncludeDeactivated) {
mutableStateOf(sharedPreferences.getBoolean(keyStatsIncludeDeactivated, false)) mutableStateOf(sharedPreferences.getBoolean(keyStatsIncludeDeactivated, false))
} }
var autoCategorize by remember(keyAutoCategorize) {
mutableStateOf(sharedPreferences.getBoolean(keyAutoCategorize, false))
}
val fullSyncDelays = SyncSettings.FULL_SYNC_DELAY_CHOICES_MINUTES val fullSyncDelays = SyncSettings.FULL_SYNC_DELAY_CHOICES_MINUTES
val fullSyncDelayLabels = listOf( 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( SettingsSwitchPreference(
title = stringResource(R.string.settings_stats_include_deactivated), title = stringResource(R.string.settings_stats_include_deactivated),
icon = Icons.Default.Group, 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( SettingsSliderPreference(
title = stringResource(R.string.settings_full_sync_delay), title = stringResource(R.string.settings_full_sync_delay),
summary = stringResource(R.string.settings_full_sync_delay_summary), summary = stringResource(R.string.settings_full_sync_delay_summary),
@@ -21,6 +21,7 @@ import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.util.BillPrefill
import net.helcel.cowspent.util.CategoryUtils import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SecureStorage import net.helcel.cowspent.util.SecureStorage
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
@@ -312,6 +313,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
db.delete(table_projects, "$key_id = ?", projectId) db.delete(table_projects, "$key_id = ?", projectId)
} }
SecureStorage.removePasswordSync(context, "ProjectPassword_$id") SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
BillPrefill.clear(context, id)
} }
// --- Members logic --- // --- Members logic ---
@@ -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<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)
}
}
@@ -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<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,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -138,8 +138,6 @@
<string name="settings_color_custom">Eigene Farbe</string> <string name="settings_color_custom">Eigene Farbe</string>
<string name="settings_color_mode">Farbauswahl</string> <string name="settings_color_mode">Farbauswahl</string>
<string name="settings_show_archived">Archivierte Projekte anzeigen</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_1h">1 Stunde</string>
<string name="pref_value_sync_1d">1 Tag</string> <string name="pref_value_sync_1d">1 Tag</string>
<string name="settings_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwende \"https\".</string> <string name="settings_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwende \"https\".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -137,8 +137,6 @@
<string name="settings_color_custom">Color personalizado</string> <string name="settings_color_custom">Color personalizado</string>
<string name="settings_color_mode">Selección de color</string> <string name="settings_color_mode">Selección de color</string>
<string name="settings_show_archived">Mostrar los proyectos archivados</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_1h">1 hora</string>
<string name="pref_value_sync_1d">1 día</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> <string name="settings_url_warn_http">ADVERTENCIA: \"http\" no es seguro. Usa \"https\".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -138,8 +138,6 @@
<string name="settings_color_custom">Couleur personnalisée</string> <string name="settings_color_custom">Couleur personnalisée</string>
<string name="settings_color_mode">Sélection de la couleur</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_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_1h">1 heure</string>
<string name="pref_value_sync_1d">1 jour</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> <string name="settings_url_warn_http">AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -137,8 +137,6 @@
<string name="settings_color_custom">Пользовательский цвет</string> <string name="settings_color_custom">Пользовательский цвет</string>
<string name="settings_color_mode">Выбор цвета</string> <string name="settings_color_mode">Выбор цвета</string>
<string name="settings_show_archived">Показывать заархивированные проекты</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_1h">1 час</string>
<string name="pref_value_sync_1d">1 день</string> <string name="pref_value_sync_1d">1 день</string>
<string name="settings_url_warn_http">ПРЕДУПРЕЖДЕНИЕ: "http" небезопасен. Используйте "https".</string> <string name="settings_url_warn_http">ПРЕДУПРЕЖДЕНИЕ: "http" небезопасен. Используйте "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
@@ -144,8 +144,6 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</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_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
+13 -4
View File
@@ -146,12 +146,20 @@
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string> <string name="settings_show_archived">Show archived projects</string>
<string name="settings_extra_features">Extra Features</string> <string name="settings_extra_features">Extra Features</string>
<string name="settings_extra_features_summary">Experimental and advanced settings. Use at your own risk.</string> <string name="settings_extra_features_summary">Experimental and advanced options.</string>
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string> <string name="prefill_button">Pre-fill</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="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_stats_include_deactivated">Include deactivated members in stats</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">Full sync delay</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="settings_full_sync_delay_summary">Time between full syncs. Other refreshes only fetch changes.</string>
<string name="pref_value_sync_always">Always</string> <string name="pref_value_sync_always">Always</string>
<string name="pref_value_sync_1h">1 hour</string> <string name="pref_value_sync_1h">1 hour</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 day</string>
@@ -178,6 +186,7 @@
<string name="pref_key_show_archived" translatable="false">showArchived</string> <string name="pref_key_show_archived" translatable="false">showArchived</string>
<string name="pref_key_extra_features" translatable="false">betaFeatures</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_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_full_sync_delay" translatable="false">fullSyncDelayMinutes</string>
<string name="pref_key_last_account_sync_timestamp" translatable="false">lastAccountSyncTimestamp</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> <string name="pref_key_fill_new_bill_from_last" translatable="false">fillNewBillFromLast</string>