Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb1ca9682
|
||
|
|
c21f0c2d7a
|
||
|
|
d8fd4aaded
|
||
|
|
a5c825c8e0
|
||
|
|
206223bff5 | ||
|
|
3deaff97fb | ||
|
|
9e6ba6e1b7 | ||
|
|
af72449037 | ||
|
|
211f751c0e | ||
|
|
bad6d59c5c | ||
|
|
7ecd10abf0 | ||
|
|
04c7deb066 | ||
|
|
c2da40ba4d | ||
|
|
6e45507b22 | ||
|
|
99d05680c5 | ||
|
|
d89d99ceff | ||
|
|
40be1b79f2 | ||
|
|
e807d6fa66 | ||
|
|
8ae8df6ef8 | ||
|
|
791f3572b6 | ||
|
|
ce8053ff2c | ||
|
|
f5f4c083f0 | ||
|
|
6800fc6c41
|
||
|
|
642b3b9c68
|
||
|
|
291694eaf4
|
||
|
|
1f56757d30 | ||
|
|
b518c2a8c2 | ||
|
|
5054525cfd | ||
|
|
47b74de720 | ||
|
|
86384dd826 | ||
|
|
625747389b | ||
|
|
bdc39d3c7e | ||
|
|
12b424679d | ||
|
|
3ba5ee4d78 | ||
|
|
c01399f45d
|
||
|
|
a9cd70ea07 | ||
|
|
73dc64a688 | ||
|
|
3b22037496 | ||
|
|
206c756ad7 | ||
|
|
a66d046b3f |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
+34
-10
@@ -8,11 +8,11 @@ env:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
tags:
|
tags:
|
||||||
- '**'
|
- "**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
@@ -34,6 +34,11 @@ jobs:
|
|||||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
|
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
|
- name: create and checkout branch
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
env:
|
env:
|
||||||
@@ -62,7 +67,8 @@ jobs:
|
|||||||
else
|
else
|
||||||
VERSION_NAME="${VERSION_BASE}"
|
VERSION_NAME="${VERSION_BASE}"
|
||||||
fi
|
fi
|
||||||
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
printf '%s\n%s\n' "$VERSION_CODE" "$VERSION_NAME" > version.txt
|
||||||
|
./gradlew bundleSignedRelease assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -71,15 +77,33 @@ 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]+)+$ ]]; 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
|
- name: Upload to Google Play
|
||||||
echo "true"
|
uses: r0adkll/upload-google-play@v1
|
||||||
fi
|
if: github.event_name == 'push'
|
||||||
|
continue-on-error: true
|
||||||
|
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
|
||||||
+70
-8
@@ -1,12 +1,74 @@
|
|||||||
# Cowspent Privacy Policy
|
# Privacy Policy for Cowspent
|
||||||
|
|
||||||
Cowspent does not collect any personal data.
|
**App:** Cowspent (`net.helcel.cowspent`)
|
||||||
This is a Free/Libre software so you can verify no data is collected
|
**Developer:** Helcel
|
||||||
by reading the source code or asking someone to do it.
|
**Effective date:** 19 September 2026
|
||||||
|
|
||||||
Members and bills are synchronized with
|
## What we collect
|
||||||
the Cospend server of your choice.
|
|
||||||
|
|
||||||
CAMERA permission is used to read QR-codes.
|
Nothing. We operate no servers and receive no data from the app. Cowspent
|
||||||
|
contains no analytics, advertising, or tracking libraries.
|
||||||
|
|
||||||
GET\_ACCOUNTS permission is used by Single-Sign-On library to access accounts from Nextcloud Files app.
|
## Data you enter and where it goes
|
||||||
|
|
||||||
|
Cowspent stores your projects, members, bills, categories, and app settings on
|
||||||
|
your device.
|
||||||
|
|
||||||
|
If you connect the app to a Cospend or IHateMoney server, this data is
|
||||||
|
synchronised with **the server you choose**: either your own or one operated by
|
||||||
|
a third party. That server's operator controls that copy of your data and their
|
||||||
|
privacy policy applies to it. We are not involved in that transfer and cannot
|
||||||
|
access it.
|
||||||
|
|
||||||
|
Your server address, username, and password or access token are stored on your
|
||||||
|
device and sent only to the server you configured, over the connection it
|
||||||
|
provides. If you sign in through the Nextcloud Files app, authentication is
|
||||||
|
handled by that app's Single Sign-On and we never see your password.
|
||||||
|
|
||||||
|
## Sharing projects
|
||||||
|
|
||||||
|
Project share links and QR codes are generated by your server. Anyone who
|
||||||
|
obtains a link or code may be able to access the project, so share them only
|
||||||
|
with people you trust.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
- `INTERNET`, `ACCESS_NETWORK_STATE` — synchronising with your chosen server.
|
||||||
|
- `CAMERA` — scanning QR codes to import a project. Images are processed on the
|
||||||
|
device, never stored or transmitted.
|
||||||
|
- `GET_ACCOUNTS` — used by the Nextcloud Single Sign-On library to list accounts
|
||||||
|
from the Nextcloud Files app.
|
||||||
|
|
||||||
|
## Retention and deletion
|
||||||
|
|
||||||
|
Data on your device remains until you delete it. Clearing the app's data in
|
||||||
|
Android Settings, or uninstalling Cowspent, removes the local copy and the
|
||||||
|
stored credentials. Data already synchronised to a server must be deleted on
|
||||||
|
that server, by you or its administrator.
|
||||||
|
|
||||||
|
## Children
|
||||||
|
|
||||||
|
Cowspent is suitable for all ages and collects no data from any user, including
|
||||||
|
children.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Credentials and project data are held in the app's private storage, protected by
|
||||||
|
the Android sandbox. Use an HTTPS server address so that synchronisation is
|
||||||
|
encrypted in transit.
|
||||||
|
|
||||||
|
## Open source
|
||||||
|
|
||||||
|
Cowspent is released under the GNU General Public License v3.0. The full source
|
||||||
|
is available at https://github.com/helcel-net/cowspent, so these statements can
|
||||||
|
be independently verified.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
Any change affecting privacy will be published in this document before or with
|
||||||
|
the release that introduces it.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Email: net-helcel+privacy@gmail.com
|
||||||
|
Issues: https://github.com/helcel-net/cowspent/issues
|
||||||
@@ -12,8 +12,6 @@ Which is originally a fork of [PhoneTrack-Android](https://gitlab.com/eneiluj/ph
|
|||||||
Which is itself a fork of [Nextcloud Notes for Android](https://github.com/stefan-niedermann/nextcloud-notes).
|
Which is itself a fork of [Nextcloud Notes for Android](https://github.com/stefan-niedermann/nextcloud-notes).
|
||||||
Many thanks to their developers :heart: !
|
Many thanks to their developers :heart: !
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="https://ko-fi.com/I2I615VP5M"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="ko-fi"></a>
|
<a href="https://ko-fi.com/I2I615VP5M"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="ko-fi"></a>
|
||||||
@@ -24,6 +22,7 @@ Many thanks to their developers :heart: !
|
|||||||
<a href="https://github.com/helcel-net/cowspent/actions/workflows/build.yml">
|
<a href="https://github.com/helcel-net/cowspent/actions/workflows/build.yml">
|
||||||
<img src="https://github.com/helcel-net/cowspent/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status">
|
<img src="https://github.com/helcel-net/cowspent/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🌄 Screenshots
|
## 🌄 Screenshots
|
||||||
@@ -57,18 +56,27 @@ Extra Features:
|
|||||||
- Support for Cospend archived-status (hide archived projects by default)
|
- Support for Cospend archived-status (hide archived projects by default)
|
||||||
- Modern UI and code with Kotlin and Jetpack-Compose
|
- Modern UI and code with Kotlin and Jetpack-Compose
|
||||||
|
|
||||||
|
|
||||||
## 📳 Installation
|
## 📳 Installation
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
|
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
|
||||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/net.helcel.cowspent">
|
<a href="https://apt.izzysoft.de/fdroid/index/apk/net.helcel.cowspent">
|
||||||
<img width="200" height="80" alt="Izzy Download" src=".github/images/izzy.png">
|
<img width="200" height="84" alt="Izzy Download" src=".github/images/izzy.png">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/helcel-net/cowspent/releases/latest">
|
<a href="https://github.com/helcel-net/cowspent/releases/latest">
|
||||||
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
|
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
|
||||||
</a>
|
</a>
|
||||||
|
<a future-href="https://f-droid.org/en/packages/net.helcel.cowspent">
|
||||||
|
<img width="200" height="84" alt="Fdroid Download" src=".github/images/fdroid.png">
|
||||||
|
</a>
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=net.helcel.cowspent">
|
||||||
|
<img width="200" height="84" alt="GooglePlay Download" src=".github/images/playstore.png">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Fdroid: requested, see https://gitlab.com/fdroid/rfp/-/work_items/4412
|
||||||
|
- PlayStore: alpha test restriction by Google. To access, join https://groups.google.com/g/helcel-android-test
|
||||||
|
|
||||||
## ⚙️ Permissions
|
## ⚙️ Permissions
|
||||||
|
|
||||||
- `CAMERA`: recommended for scanning barcodes from camera
|
- `CAMERA`: recommended for scanning barcodes from camera
|
||||||
@@ -88,6 +96,13 @@ Cowspent is a user-driven project. We welcome any contribution, big or small.
|
|||||||
|
|
||||||
Thanks to all contributors, the developers of our dependencies, and our users.
|
Thanks to all contributors, the developers of our dependencies, and our users.
|
||||||
|
|
||||||
|
## Signinig key
|
||||||
|
|
||||||
|
```
|
||||||
|
net.helcel.cowspent
|
||||||
|
D3:EE:68:A8:5A:7F:35:A1:14:D2:74:B4:50:AA:CE:8C:ED:68:EA:C8:1A:A2:22:E2:7F:F5:0E:98:4C:C6:29
|
||||||
|
```
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-11
@@ -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'
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
CUE6FCSBR6JIAAAAAAAAAAAAAA
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package net.helcel.cowspent.android.bill_edit
|
package net.helcel.cowspent.android.bill_edit
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
@@ -19,18 +22,24 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -40,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
|
||||||
@@ -57,11 +67,15 @@ 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
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var amountFocused by remember { mutableStateOf(false) }
|
||||||
|
var amountSelection by remember { mutableStateOf(TextRange(viewModel.amount.length)) }
|
||||||
|
|
||||||
// The activity reads the project off the main thread and only then reports whether this is a
|
// The activity reads the project off the main thread and only then reports whether this is a
|
||||||
// new bill, so the flag arrives after the first composition. Keying on it rather than on Unit
|
// new bill, so the flag arrives after the first composition. Keying on it rather than on Unit
|
||||||
@@ -91,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)
|
||||||
}
|
}
|
||||||
@@ -118,7 +140,12 @@ fun EditBillScreen(
|
|||||||
val errorOwers = stringResource(R.string.error_invalid_bill_owers)
|
val errorOwers = stringResource(R.string.error_invalid_bill_owers)
|
||||||
val errorInvalidForm = stringResource(R.string.error_generic)
|
val errorInvalidForm = stringResource(R.string.error_generic)
|
||||||
|
|
||||||
FloatingActionButton(onClick = {
|
FloatingActionButton(
|
||||||
|
// Keep the button clear of the operator row pinned above the keyboard.
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
bottom = if (amountFocused) MathOperatorRowHeight else 0.dp
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
val validationError = viewModel.getValidationError(
|
val validationError = viewModel.getValidationError(
|
||||||
errorWhat, errorDate, errorPayer, errorOwers, errorInvalidForm
|
errorWhat, errorDate, errorPayer, errorOwers, errorInvalidForm
|
||||||
)
|
)
|
||||||
@@ -141,8 +168,12 @@ fun EditBillScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(16.dp)
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
BillBasicInfoSection(
|
BillBasicInfoSection(
|
||||||
@@ -150,7 +181,10 @@ fun EditBillScreen(
|
|||||||
canEdit = canEdit,
|
canEdit = canEdit,
|
||||||
onDateClick = onDateClick,
|
onDateClick = onDateClick,
|
||||||
onTimeClick = onTimeClick,
|
onTimeClick = onTimeClick,
|
||||||
amountFocusRequester = focusRequester
|
amountFocusRequester = focusRequester,
|
||||||
|
amountSelection = amountSelection,
|
||||||
|
onAmountSelectionChange = { amountSelection = it },
|
||||||
|
onAmountFocusChange = { amountFocused = it }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -178,6 +212,154 @@ fun EditBillScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (amountFocused && canEdit) {
|
||||||
|
MathOperatorRow(onInsert = { op ->
|
||||||
|
val text = viewModel.amount
|
||||||
|
val start = amountSelection.min.coerceIn(0, text.length)
|
||||||
|
val end = amountSelection.max.coerceIn(0, text.length)
|
||||||
|
viewModel.amount = text.replaceRange(start, end, op)
|
||||||
|
amountSelection = TextRange(start + op.length)
|
||||||
|
viewModel.updateSplits()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
private fun keyboardColors(): KeyboardColors {
|
||||||
|
val dark = isSystemInDarkTheme()
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val (tray, edge) = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> if (dark) {
|
||||||
|
colorResource(android.R.color.system_surface_container_dark) to
|
||||||
|
colorResource(android.R.color.system_surface_container_high_dark)
|
||||||
|
} else {
|
||||||
|
colorResource(android.R.color.system_surface_container_light) to
|
||||||
|
colorResource(android.R.color.system_surface_container_high_light)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> if (dark) {
|
||||||
|
colorResource(android.R.color.system_neutral1_900) to
|
||||||
|
colorResource(android.R.color.system_neutral1_800)
|
||||||
|
} else {
|
||||||
|
colorResource(android.R.color.system_neutral1_50) to
|
||||||
|
colorResource(android.R.color.system_neutral1_100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dark) KeyboardColors(
|
||||||
|
tray = tray,
|
||||||
|
edge = edge,
|
||||||
|
key = colorResource(android.R.color.system_accent2_700),
|
||||||
|
label = colorResource(android.R.color.system_neutral1_50)
|
||||||
|
) else KeyboardColors(
|
||||||
|
tray = tray,
|
||||||
|
edge = edge,
|
||||||
|
key = colorResource(android.R.color.system_accent2_100),
|
||||||
|
label = colorResource(android.R.color.system_neutral1_900)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (dark) KeyboardColors(Color(0xFF1B1B1F), Color(0xFF2B2B2F), Color(0xFF43474E), Color(0xFFE3E2E6))
|
||||||
|
else KeyboardColors(Color(0xFFEEEDF6), Color(0xFFE7E7F0), Color(0xFFDDE2F9), Color(0xFF1B1B1F))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Operator keys shown above the numeric keypad, which has no room for them itself. */
|
||||||
|
@Composable
|
||||||
|
fun MathOperatorRow(onInsert: (String) -> Unit) {
|
||||||
|
val colors = keyboardColors()
|
||||||
|
Surface(color = colors.tray) {
|
||||||
|
// Flat pill-shaped keys, styled like the keyboard's own function keys (e.g. its minus).
|
||||||
|
val keyShape = RoundedCornerShape(percent = 50)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(MathOperatorRowHeight)
|
||||||
|
.drawBehind {
|
||||||
|
// A thin edge in the same tone the keyboard uses for its own top border.
|
||||||
|
drawLine(colors.edge, Offset(0f, 0f), Offset(size.width, 0f), 1.dp.toPx())
|
||||||
|
}
|
||||||
|
.padding(horizontal = 3.dp, vertical = 5.dp)
|
||||||
|
) {
|
||||||
|
listOf("(" to "(", ")" to ")", "+" to "+", "−" to "-", "×" to "*", "÷" to "/").forEach { (label, op) ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = 3.dp)
|
||||||
|
.clip(keyShape)
|
||||||
|
.background(colors.key)
|
||||||
|
.clickable { onInsert(op) },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(label, fontSize = 24.sp, color = colors.label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +369,10 @@ fun BillBasicInfoSection(
|
|||||||
canEdit: Boolean,
|
canEdit: Boolean,
|
||||||
onDateClick: () -> Unit,
|
onDateClick: () -> Unit,
|
||||||
onTimeClick: () -> Unit,
|
onTimeClick: () -> Unit,
|
||||||
amountFocusRequester: FocusRequester
|
amountFocusRequester: FocusRequester,
|
||||||
|
amountSelection: TextRange = TextRange(viewModel.amount.length),
|
||||||
|
onAmountSelectionChange: (TextRange) -> Unit = {},
|
||||||
|
onAmountFocusChange: (Boolean) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "GENERAL",
|
text = "GENERAL",
|
||||||
@@ -203,15 +388,29 @@ fun BillBasicInfoSection(
|
|||||||
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
|
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.amount,
|
value = TextFieldValue(
|
||||||
|
text = viewModel.amount,
|
||||||
|
selection = TextRange(
|
||||||
|
amountSelection.start.coerceIn(0, viewModel.amount.length),
|
||||||
|
amountSelection.end.coerceIn(0, viewModel.amount.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
onValueChange = { nv ->
|
onValueChange = { nv ->
|
||||||
val filteredValue = nv.filter { it in "0123456789.+-*/" }
|
val filteredValue = nv.text.filter { it in "0123456789.,+-*/()" }
|
||||||
|
onAmountSelectionChange(
|
||||||
|
if (filteredValue == nv.text) nv.selection else TextRange(filteredValue.length)
|
||||||
|
)
|
||||||
|
if (filteredValue != viewModel.amount) {
|
||||||
viewModel.amount = filteredValue
|
viewModel.amount = filteredValue
|
||||||
viewModel.updateSplits()
|
viewModel.updateSplits()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
enabled = canEdit,
|
enabled = canEdit,
|
||||||
placeholder = { Text("0") },
|
placeholder = { Text("0") },
|
||||||
modifier = Modifier.fillMaxWidth().focusRequester(amountFocusRequester),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(amountFocusRequester)
|
||||||
|
.onFocusChanged { onAmountFocusChange(it.isFocused) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
|
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
|
||||||
viewModel.mainCurrencyName.ifEmpty { "$" }
|
viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||||
@@ -245,7 +444,7 @@ fun BillBasicInfoSection(
|
|||||||
Icon(Icons.Default.SwapHoriz, contentDescription = "Change Currency")
|
Icon(Icons.Default.SwapHoriz, contentDescription = "Change Currency")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone, imeAction = ImeAction.Next),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Next),
|
||||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -253,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(),
|
||||||
@@ -473,7 +672,7 @@ fun OwerSelectionSection(
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = { nv ->
|
onValueChange = { nv ->
|
||||||
val filteredValue = nv.filter { it in "0123456789.+-*/" }
|
val filteredValue = nv.filter { it in "0123456789,.+-*/" }
|
||||||
if (viewModel.splitMode == SplitMode.PERCENT) {
|
if (viewModel.splitMode == SplitMode.PERCENT) {
|
||||||
viewModel.owersPercentSplit[member.id] = filteredValue
|
viewModel.owersPercentSplit[member.id] = filteredValue
|
||||||
} else {
|
} else {
|
||||||
@@ -557,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)
|
||||||
@@ -566,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)
|
||||||
@@ -576,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("")
|
||||||
@@ -48,11 +63,12 @@ class EditBillViewModel : ViewModel() {
|
|||||||
if (input.isBlank()) return 0.0
|
if (input.isBlank()) return 0.0
|
||||||
val sanitized = input.replace(',', '.')
|
val sanitized = input.replace(',', '.')
|
||||||
// If it contains any math operator, try evalMath first
|
// If it contains any math operator, try evalMath first
|
||||||
if (sanitized.any { it in "+-*/" }) {
|
if (sanitized.any { it in "+-*/()" }) {
|
||||||
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) {
|
||||||
|
|||||||
+9
-1
@@ -48,8 +48,10 @@ import net.helcel.cowspent.model.Transaction
|
|||||||
import net.helcel.cowspent.model.UserItem
|
import net.helcel.cowspent.model.UserItem
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
import net.helcel.cowspent.util.SupportUtil
|
import net.helcel.cowspent.util.SupportUtil
|
||||||
|
import net.helcel.cowspent.util.SupportUtil.SETTLE_DIRECT
|
||||||
import net.helcel.cowspent.util.SupportUtil.SETTLE_OPTIMAL
|
import net.helcel.cowspent.util.SupportUtil.SETTLE_OPTIMAL
|
||||||
import net.helcel.cowspent.util.SupportUtil.settleBills
|
import net.helcel.cowspent.util.SupportUtil.settleBills
|
||||||
|
import net.helcel.cowspent.util.SupportUtil.settleBillsDirect
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProjectSettlementDialogContent(
|
fun ProjectSettlementDialogContent(
|
||||||
@@ -60,10 +62,12 @@ fun ProjectSettlementDialogContent(
|
|||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val centerNoneStr = stringResource(R.string.center_none)
|
val centerNoneStr = stringResource(R.string.center_none)
|
||||||
|
val centerDirectStr = stringResource(R.string.center_none_direct)
|
||||||
val memberList = remember(proj.id) { db.getMembersOfProject(proj.id, null) }
|
val memberList = remember(proj.id) { db.getMembersOfProject(proj.id, null) }
|
||||||
val userList = remember(memberList, centerNoneStr) {
|
val userList = remember(memberList, centerNoneStr, centerDirectStr) {
|
||||||
buildList {
|
buildList {
|
||||||
add(UserItem(SETTLE_OPTIMAL, centerNoneStr))
|
add(UserItem(SETTLE_OPTIMAL, centerNoneStr))
|
||||||
|
add(UserItem(SETTLE_DIRECT, centerDirectStr))
|
||||||
addAll(memberList.map { UserItem(it.id, it.name) })
|
addAll(memberList.map { UserItem(it.id, it.name) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,8 +95,12 @@ fun ProjectSettlementDialogContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val transactions = remember(selectedMemberId, membersBalance, membersSortedByName) {
|
val transactions = remember(selectedMemberId, membersBalance, membersSortedByName) {
|
||||||
|
if (selectedMemberId == SETTLE_DIRECT) {
|
||||||
|
settleBillsDirect(membersSortedByName, db.getBillsOfProject(proj.id))
|
||||||
|
} else {
|
||||||
settleBills(membersSortedByName, membersBalance, selectedMemberId)
|
settleBills(membersSortedByName, membersBalance, selectedMemberId)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ProjectSettlementUI(
|
ProjectSettlementUI(
|
||||||
transactions = transactions,
|
transactions = transactions,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,6 +167,39 @@ object SupportUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val SETTLE_OPTIMAL: Long = 0
|
const val SETTLE_OPTIMAL: Long = 0
|
||||||
|
const val SETTLE_DIRECT: Long = -1
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun settleBillsDirect(members: List<DBMember>, bills: List<DBBill>): List<Transaction> {
|
||||||
|
val membersWeight = members.associate { it.id to it.weight }
|
||||||
|
// owed[debtor][creditor] = total the debtor owes the creditor over all bills
|
||||||
|
val owed = HashMap<Long, HashMap<Long, Double>>()
|
||||||
|
|
||||||
|
for (b in bills) {
|
||||||
|
if (b.state == DBBill.STATE_DELETED) continue
|
||||||
|
val nbOwerShares = b.billOwers.sumOf { membersWeight[it.memberId] ?: 0.0 }
|
||||||
|
if (nbOwerShares <= 0.0) continue
|
||||||
|
for (bo in b.billOwers) {
|
||||||
|
if (bo.memberId == b.payerId) continue
|
||||||
|
val share = b.amount / nbOwerShares * (membersWeight[bo.memberId] ?: 0.0)
|
||||||
|
val row = owed.getOrPut(bo.memberId) { HashMap() }
|
||||||
|
row[b.payerId] = (row[b.payerId] ?: 0.0) + share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val results: MutableList<Transaction> = ArrayList()
|
||||||
|
for (debtor in members) {
|
||||||
|
for (creditor in members) {
|
||||||
|
if (debtor.id == creditor.id) continue
|
||||||
|
val net = (owed[debtor.id]?.get(creditor.id) ?: 0.0) -
|
||||||
|
(owed[creditor.id]?.get(debtor.id) ?: 0.0)
|
||||||
|
if (round2(net) > 0.0) {
|
||||||
|
results.add(Transaction(debtor.id, creditor.id, round2(net)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun settleBills(
|
fun settleBills(
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -209,6 +207,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Keine (optimal)</string>
|
<string name="center_none">Keine (optimal)</string>
|
||||||
|
<string name="center_none_direct">Keine (direkt)</string>
|
||||||
<string name="settle_who">Wer zahlt</string>
|
<string name="settle_who">Wer zahlt</string>
|
||||||
<string name="settle_to_whom">An wen</string>
|
<string name="settle_to_whom">An wen</string>
|
||||||
<string name="settle_how_much">Betrag</string>
|
<string name="settle_how_much">Betrag</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>
|
||||||
@@ -210,6 +208,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Ninguno (óptimo)</string>
|
<string name="center_none">Ninguno (óptimo)</string>
|
||||||
|
<string name="center_none_direct">Ninguno (directo)</string>
|
||||||
<string name="settle_who">Quién paga</string>
|
<string name="settle_who">Quién paga</string>
|
||||||
<string name="settle_to_whom">A quién</string>
|
<string name="settle_to_whom">A quién</string>
|
||||||
<string name="settle_how_much">Importe</string>
|
<string name="settle_how_much">Importe</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>
|
||||||
@@ -213,6 +211,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Aucun (optimal)</string>
|
<string name="center_none">Aucun (optimal)</string>
|
||||||
|
<string name="center_none_direct">Aucun (direct)</string>
|
||||||
<string name="settle_who">Qui paie</string>
|
<string name="settle_who">Qui paie</string>
|
||||||
<string name="settle_to_whom">À qui</string>
|
<string name="settle_to_whom">À qui</string>
|
||||||
<string name="settle_how_much">Montant</string>
|
<string name="settle_how_much">Montant</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>
|
||||||
@@ -209,6 +207,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Нет (оптимально)</string>
|
<string name="center_none">Нет (оптимально)</string>
|
||||||
|
<string name="center_none_direct">Нет (напрямую)</string>
|
||||||
<string name="settle_who">Кто платит</string>
|
<string name="settle_who">Кто платит</string>
|
||||||
<string name="settle_to_whom">Кому</string>
|
<string name="settle_to_whom">Кому</string>
|
||||||
<string name="settle_how_much">Сумма</string>
|
<string name="settle_how_much">Сумма</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -247,6 +256,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">None (Optimal)</string>
|
<string name="center_none">None (Optimal)</string>
|
||||||
|
<string name="center_none_direct">None (Direct)</string>
|
||||||
<string name="settle_who">Who pays</string>
|
<string name="settle_who">Who pays</string>
|
||||||
<string name="settle_to_whom">To whom</string>
|
<string name="settle_to_whom">To whom</string>
|
||||||
<string name="settle_how_much">Amount</string>
|
<string name="settle_how_much">Amount</string>
|
||||||
|
|||||||
Reference in new issue
Block a user