23 Commits
Author SHA1 Message Date
sora 8ae8df6ef8 Merge pull request 'Update dependency org.robolectric:robolectric to v4.17' (#19) from renovate/org.robolectric-robolectric-4.x into main
Reviewed-on: #19
2026-09-30 23:02:53 +02:00
sora 791f3572b6 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.09.00' (#18) from renovate/androidx.compose-compose-bom-2026.x into main
Reviewed-on: #18
2026-09-30 23:02:34 +02:00
sora ce8053ff2c Merge pull request 'Update dependency androidx.compose.foundation:foundation to v1.12.1' (#16) from renovate/androidx.compose.foundation-foundation-1.x into main
Reviewed-on: #16
2026-09-30 23:02:15 +02:00
sora f5f4c083f0 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.20' (#14) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main
Reviewed-on: #14
2026-09-30 23:01:39 +02:00
sora 6800fc6c41 Direct Settlement 2026-09-30 22:51:10 +02:00
sora 642b3b9c68 Fix keyboard (and add calculator bar) 2026-09-30 22:41:10 +02:00
sora 291694eaf4 Allow Comma values 2026-09-30 22:18:25 +02:00
sora b518c2a8c2 Update .github/workflows/build.yml 2026-09-20 09:00:12 +02:00
sora 5054525cfd Update README.md 2026-09-19 18:11:05 +02:00
sora 47b74de720 Upload files to ".github/images" 2026-09-19 17:59:41 +02:00
sora 86384dd826 Upload files to ".github/images" 2026-09-19 17:50:35 +02:00
sora 625747389b Update .github/workflows/build.yml 2026-09-19 12:20:29 +02:00
sora bdc39d3c7e Update PRIVACY.md 2026-09-19 10:27:15 +02:00
sora 12b424679d Update .github/workflows/build.yml 2026-09-18 23:40:59 +02:00
sora 3ba5ee4d78 Add app/src/main/assets/adi-registration.properties 2026-09-18 12:20:29 +02:00
sora c01399f45d moved signing key to the end 2026-09-16 23:57:15 +02:00
gtcrowdandGitHub a9cd70ea07 Update README.md to include the signing key
For security and also to have a source of true for AppVerifierBG and https://github.com/privacyguides/verified-apps/
2026-09-11 10:24:33 +00:00
Renovate Bot 73dc64a688 Update dependency org.robolectric:robolectric to v4.17 2026-09-11 02:01:48 +00:00
Renovate Bot 3b22037496 Update dependency androidx.compose:compose-bom to v2026.09.00 2026-09-11 02:01:46 +00:00
sora c21a2b05ef Merge remote-tracking branch 'gh/pr/22' into fix/large-project-sync-feedback 2026-09-11 01:15:34 +02:00
KirillandGitHub 7ffda1c2f9 Russian translations proposal 2026-09-10 15:05:27 +03:00
Renovate Bot 206c756ad7 Update dependency androidx.compose.foundation:foundation to v1.12.1 2026-09-10 02:01:21 +00:00
Renovate Bot a66d046b3f Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.20 2026-09-08 02:01:15 +00:00
16 changed files with 563 additions and 300 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+19 -1
View File
@@ -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,9 @@ jobs:
else else
VERSION_NAME="${VERSION_BASE}" VERSION_NAME="${VERSION_BASE}"
fi fi
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME ./gradlew bundleSignedRelease assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
# ./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
# ./gradlew bundleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
@@ -83,3 +90,14 @@ jobs:
else else
echo "true" echo "true"
fi fi
- name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
if: startsWith(github.ref, 'refs/tags/')
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: net.helcel.cowspent
releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab
tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'alpha' }}
status: completed
whatsNewDirectory: tmp/whatsnew
+70 -8
View File
@@ -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
+19 -4
View File
@@ -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
``` ```
+5 -5
View File
@@ -1,7 +1,7 @@
plugins { plugins {
id 'com.android.application' version '9.3.2' id 'com.android.application' version '9.3.2'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10' id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10'
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,7 +118,7 @@ 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.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
@@ -132,7 +132,7 @@ dependencies {
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.4' implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.4'
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'
@@ -147,12 +147,12 @@ dependencies {
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
@@ -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
@@ -62,6 +71,8 @@ fun EditBillScreen(
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
@@ -118,7 +129,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 +157,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 +170,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 +201,95 @@ 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
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 +299,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 +318,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 +374,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) })
) )
@@ -473,7 +602,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 {
@@ -48,7 +48,7 @@ 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)
@@ -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,
@@ -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(
@@ -209,6 +209,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>
@@ -210,6 +210,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>
@@ -213,6 +213,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>
+223 -230
View File
@@ -1,266 +1,259 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!--
Untranslated: these are the English strings, kept here so a translation can be
contributed by simply replacing the values. Strings marked translatable="false" in
values/strings.xml (app and product names, preference keys) are deliberately absent
and must not be added.
-->
<!-- Actions --> <!-- Actions -->
<string name="action_new_bill">New bill</string> <string name="action_new_bill">Новый расход</string>
<string name="action_add_project">Add project</string> <string name="action_add_project">Добавить проект</string>
<string name="action_save">Save</string> <string name="action_save">Сохранить</string>
<string name="action_edit">Edit</string> <string name="action_edit">Редактировать</string>
<string name="action_share">Share</string> <string name="action_share">Поделиться</string>
<string name="action_search">Search</string> <string name="action_search">Поиск</string>
<string name="action_open_menu">Open menu</string> <string name="action_open_menu">Открыть меню</string>
<string name="action_close_search">Close search</string> <string name="action_close_search">Закрыть поиск</string>
<string name="action_clear_search">Clear search</string> <string name="action_clear_search">Очистить поиск</string>
<string name="action_delete">Delete</string> <string name="action_delete">Удалить</string>
<string name="simple_back">Back</string> <string name="simple_back">Назад</string>
<string name="action_archive">Archive</string> <string name="action_archive">Архивировать</string>
<string name="action_unarchive">Unarchive</string> <string name="action_unarchive">Разархивировать</string>
<string name="action_export">Export</string> <string name="action_export">Экспорт</string>
<string name="action_stats">Stats</string> <string name="action_stats">Статистика</string>
<string name="action_settle">Settle</string> <string name="action_settle">Свести баланс</string>
<string name="action_scan_qrcode">Scan QR Code</string> <string name="action_scan_qrcode">Отсканировать QR-код</string>
<string name="action_settings">Settings</string> <string name="action_settings">Настройки</string>
<string name="action_label_bills">Label missing categories</string> <string name="action_label_bills">Указать отсутствующие категории</string>
<string name="action_logout">Logout</string> <string name="action_logout">Выйти</string>
<string name="action_connect">Connect</string> <string name="action_connect">Подключиться</string>
<string name="action_discard">Discard</string> <string name="action_discard">Отменить</string>
<string name="action_members">Members</string> <string name="action_members">Участники</string>
<string name="action_labels">Labels</string> <string name="action_labels">Метки</string>
<string name="action_currencies">Currencies</string> <string name="action_currencies">Валюты</string>
<!-- Titles --> <!-- Titles -->
<string name="title_stats">Statistics</string> <string name="title_stats">Статистика</string>
<string name="title_edit_project">Edit project</string> <string name="title_edit_project">Редактировать проект</string>
<string name="title_label_bills">Label Bills</string> <string name="title_label_bills">Указать категории</string>
<string name="title_labels">Manage Labels</string> <string name="title_labels">Управление метками</string>
<string name="title_about">About</string> <string name="title_about">О программе</string>
<string name="title_settle">Settle Project</string> <string name="title_settle">Сведение баланса</string>
<string name="title_share">Share Project</string> <string name="title_share">Поделиться проектом</string>
<string name="title_add_project">Add Project</string> <string name="title_add_project">Добавить проект</string>
<string name="title_add_category">Add Category</string> <string name="title_add_category">Добавить категорию</string>
<string name="title_add_payment_mode">Add Payment Mode</string> <string name="title_add_payment_mode">Добавить способ оплаты</string>
<string name="title_account">Nextcloud Account</string> <string name="title_account">Учётная запись Nextcloud</string>
<string name="title_share_web">Web link</string> <string name="title_share_web">Веб-ссылка</string>
<string name="title_share_qr">Cowspent link</string> <string name="title_share_qr">Ссылка Cowspent</string>
<string name="title_confirm">Are you sure?</string> <string name="title_confirm">Вы уверены?</string>
<!-- Labels and Fields --> <!-- Labels and Fields -->
<string name="label_all_bills">All bills</string> <string name="label_all_bills">Все расходы</string>
<string name="label_categories">Categories</string> <string name="label_categories">Категории</string>
<string name="label_payment_modes">Payment Modes</string> <string name="label_payment_modes">Способы оплаты</string>
<string name="label_name">Name</string> <string name="label_name">Название</string>
<string name="label_icon">Icon / Emoji</string> <string name="label_icon">Значок / Смайлик</string>
<string name="label_color">Color</string> <string name="label_color">Цвет</string>
<string name="label_weight">Weight</string> <string name="label_weight">Вес</string>
<string name="label_activated">Activated</string> <string name="label_activated">Активировано</string>
<string name="label_password">Password</string> <string name="label_password">Пароль</string>
<string name="label_email">E-mail</string> <string name="label_email">Электронная почта</string>
<string name="label_url">Server address</string> <string name="label_url">Адрес сервера</string>
<string name="label_username">Username</string> <string name="label_username">Имя пользователя</string>
<string name="label_comment">Comment</string> <string name="label_comment">Комментарий</string>
<string name="label_what">What?</string> <string name="label_what">Что?</string>
<string name="label_payer">Who paid?</string> <string name="label_payer">Кто оплатил?</string>
<string name="label_owers">For whom?</string> <string name="label_owers">Для кого?</string>
<string name="label_repeat">Repeat every</string> <string name="label_repeat">Повторять каждые</string>
<string name="label_mode">Mode</string> <string name="label_mode">Режим</string>
<string name="label_category">Category</string> <string name="label_category">Категория</string>
<string name="label_project_id">Project ID/name</string> <string name="label_project_id">Идентификатор/название проекта</string>
<string name="label_project_title">Project title</string> <string name="label_project_title">Название проекта</string>
<string name="label_use_sso">Use Nextcloud App Account</string> <string name="label_use_sso">Использовать учётную запись приложения Nextcloud</string>
<!-- Dialogs and Messages --> <!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string> <string name="dialog_unsaved_changes_title">Несохраненные изменения</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string> <string name="dialog_unsaved_changes_msg">Сохранить изменения перед выходом?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string> <string name="dialog_confirm_remove_project_msg">Проект на сервере не будет удален.</string>
<string name="dialog_sync_error_title">Sync error</string> <string name="dialog_sync_error_title">Ошибка синхронизации</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string> <string name="dialog_sync_error_msg">Не удалось синхронизировать %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string> <string name="dialog_balanced_msg">Расходы уже сбалансированы.</string>
<string name="msg_project_added">Project %1$s added</string> <string name="msg_project_added">Проект %1$s добавлен</string>
<string name="msg_bill_labeled_done">All bills labeled</string> <string name="msg_bill_labeled_done">Все расходы помечены</string>
<string name="msg_no_suggestions">No suggestions</string> <string name="msg_no_suggestions">Нет предложений</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string> <string name="msg_auth_warning">Требуется Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string> <string name="msg_link_copied">Ссылка скопирована в буфер обмена</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string> <string name="msg_share_qr">Отсканируйте QR-код или поделитесь ссылкой, чтобы присоединиться.</string>
<string name="msg_share_web">Link for web browser access.</string> <string name="msg_share_web">Ссылка для доступа через веб-браузер.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string> <string name="msg_share_qr_warn">Поделитесь этой ссылкой с пользователем Cowspent.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string> <string name="msg_settle_intro">Расчет по %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string> <string name="msg_settle_sentence">%1$s должен %2$s %3$.2f</string>
<string name="msg_stats_intro">Stats for %1$s:</string> <string name="msg_stats_intro">Статистика для %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string> <string name="msg_stats_header">Участник (Оплачено | Потрачено | Баланс)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string> <string name="msg_logged_in_as">Вошёл под именем %1$s</string>
<!-- Errors --> <!-- Errors -->
<string name="error_generic">Error</string> <string name="error_generic">Ошибка</string>
<string name="error_loading">Loading</string> <string name="error_loading">Загрузка</string>
<string name="error_no_projects">No projects found</string> <string name="error_no_projects">Проекты не найдены</string>
<string name="error_no_members">No members found</string> <string name="error_no_members">Участники не найдены</string>
<string name="error_no_bills">No bills found</string> <string name="error_no_bills">Расходы не найдены</string>
<string name="error_no_member">At least one member required</string> <string name="error_no_member">Требуется как минимум один участник</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string> <string name="error_maintenance_mode">Сервер находится в режиме технического обслуживания</string>
<string name="error_400">400 Bad request</string> <string name="error_400">400 Неверный запрос</string>
<string name="error_401">401 Unauthorized</string> <string name="error_401">401 Неавторизованный доступ</string>
<string name="error_403">403 Forbidden</string> <string name="error_403">403 Доступ запрещен</string>
<string name="error_404">404 Not Found</string> <string name="error_404">404 Не найдено</string>
<string name="error_sync">Sync failed: %1$s</string> <string name="error_sync">Сбой синхронизации: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string> <string name="error_invalid_login">Неверный логин: %1$s</string>
<string name="error_auth">Wrong username or password</string> <string name="error_auth">Неверное имя пользователя или пароль</string>
<string name="error_json">Invalid server response</string> <string name="error_json">Неверный ответ сервера</string>
<string name="error_req_failed">Request failed</string> <string name="error_req_failed">Ошибка запроса</string>
<string name="error_invalid_email">Invalid e-mail</string> <string name="error_invalid_email">Неверный адрес электронной почты</string>
<string name="error_invalid_project_id">Invalid project ID</string> <string name="error_invalid_project_id">Неверный идентификатор проекта</string>
<string name="error_invalid_project_name">Invalid project title</string> <string name="error_invalid_project_name">Неверное название проекта</string>
<string name="error_invalid_bill_name">Invalid bill name</string> <string name="error_invalid_bill_name">Неверное название расхода</string>
<string name="error_invalid_bill_date">Invalid bill date</string> <string name="error_invalid_bill_date">Неверная дата расхода</string>
<string name="error_invalid_bill_payer">Payer required</string> <string name="error_invalid_bill_payer">Требуется указать плательщика</string>
<string name="error_invalid_bill_owers">Owers required</string> <string name="error_invalid_bill_owers">Требуется указать участников</string>
<string name="error_no_network">No network connection</string> <string name="error_no_network">Отсутствует подключение к сети</string>
<string name="error_server">Server error</string> <string name="error_server">Ошибка сервера</string>
<string name="error_io">Server connection broken</string> <string name="error_io">Сбой соединения с сервером</string>
<string name="error_share_impossible">Cannot share this project</string> <string name="error_share_impossible">Невозможно поделиться этим проектом</string>
<!-- Drawer / Common UI --> <!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string> <string name="drawer_no_account">Подключиться к учетной записи Nextcloud</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string> <string name="drawer_last_sync">Последняя синхронизация: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string> <string name="simple_cancel">Отменить</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string> <string name="simple_ok" tools:ignore="ButtonCase,Typos">ОК</string>
<string name="simple_yes">Yes</string> <string name="simple_yes">Да</string>
<string name="simple_no">No</string> <string name="simple_no">Нет</string>
<string name="simple_close">Close</string> <string name="simple_close">Закрыть</string>
<!-- Settings --> <!-- Settings -->
<string name="settings_appearance">Appearance</string> <string name="settings_appearance">Внешний вид</string>
<string name="settings_network">Network</string> <string name="settings_network">Сеть</string>
<string name="settings_other">Other</string> <string name="settings_other">Прочее</string>
<string name="settings_night_mode">Theme</string> <string name="settings_night_mode">Тема</string>
<string name="settings_offline_mode">Offline mode</string> <string name="settings_offline_mode">Офлайн-режим</string>
<string name="settings_offline_mode_summary">Only sync manually.</string> <string name="settings_offline_mode_summary">Синхронизация только вручную</string>
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Пользовательский цвет</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Выбор цвета</string>
<string name="settings_show_archived">Show archived projects</string> <string name="settings_show_archived">Показывать заархивированные проекты</string>
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string> <string name="settings_fill_new_bill_from_last">Автозаполнение на основе последнего расхода</string>
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string> <string name="settings_fill_new_bill_from_last_summary">Предварительное заполнение полей "Плательщик", "Категория", "Способ оплаты" и "Для кого" на основе последнего расхода, созданного в проекте.</string>
<string name="pref_value_sync_1h">1 hour</string> <string name="pref_value_sync_1h">1 час</string>
<string name="pref_value_sync_1d">1 day</string> <string name="pref_value_sync_1d">1 день</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">ПРЕДУПРЕЖДЕНИЕ: "http" небезопасен. Используйте "https".</string>
<string name="settings_colorpicker_title">Choose Color</string> <string name="settings_colorpicker_title">Выбрать цвет</string>
<string name="pref_value_color_system">Системная</string>
<string name="pref_value_color_system">System</string> <string name="pref_value_color_manual">Вручную</string>
<string name="pref_value_color_manual">Manual</string> <string name="pref_value_theme_light">Светлая</string>
<string name="pref_value_theme_light">Light</string> <string name="pref_value_theme_dark">Тёмная</string>
<string name="pref_value_theme_dark">Dark</string> <string name="pref_value_theme_system">По системе</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Enums and Lists --> <!-- Enums and Lists -->
<string name="repeat_no">No repeat</string> <string name="repeat_no">Без повторения</string>
<string name="repeat_day">Daily</string> <string name="repeat_day">Ежедневно</string>
<string name="repeat_week">Weekly</string> <string name="repeat_week">Еженедельно</string>
<string name="repeat_fortnight">Fortnightly</string> <string name="repeat_fortnight">Раз в две недели</string>
<string name="repeat_month">Monthly</string> <string name="repeat_month">Ежемесячно</string>
<string name="repeat_year">Yearly</string> <string name="repeat_year">Ежегодно</string>
<string name="payment_mode_none">None</string> <string name="payment_mode_none">Нет</string>
<string name="payment_mode_all">All</string> <string name="payment_mode_all">Все</string>
<string name="payment_mode_credit_card">Credit card</string> <string name="payment_mode_credit_card">Кредитная карта</string>
<string name="payment_mode_cash">Cash</string> <string name="payment_mode_cash">Наличные</string>
<string name="payment_mode_check">Check</string> <string name="payment_mode_check">Чек</string>
<string name="payment_mode_online">Online</string> <string name="payment_mode_online">Онлайн</string>
<string name="payment_mode_transfer">Transfer</string> <string name="payment_mode_transfer">Перевод</string>
<string name="category_none">None</string> <string name="category_none">Нет</string>
<string name="category_all">All</string> <string name="category_all">Все</string>
<string name="category_all_except_reimbursement">All except reimbursement</string> <string name="category_all_except_reimbursement">Все, кроме возмещения</string>
<string name="category_groceries">Grocery</string> <string name="category_groceries">Продукты</string>
<string name="category_leisure">Bar/Party</string> <string name="category_leisure">Бар/Вечеринка</string>
<string name="category_rent">Rent</string> <string name="category_rent">Аренда</string>
<string name="category_bills">Bill</string> <string name="category_bills">Счета</string>
<string name="category_excursion">Excursion/Culture</string> <string name="category_excursion">Экскурсии/Культура</string>
<string name="category_health">Health</string> <string name="category_health">Здоровье</string>
<string name="category_shopping">Shopping</string> <string name="category_shopping">Покупки</string>
<string name="category_reimbursement">Reimbursement</string> <string name="category_reimbursement">Возмещение расходов</string>
<string name="category_restaurant">Restaurant</string> <string name="category_restaurant">Ресторан</string>
<string name="category_accomodation">Accommodation</string> <string name="category_accomodation">Проживание</string>
<string name="category_transport">Transport</string> <string name="category_transport">Транспорт</string>
<string name="category_sport">Sport</string> <string name="category_sport">Спорт</string>
<!-- Project specific --> <!-- Project specific -->
<string name="new_project_action">What</string> <string name="new_project_action">Что</string>
<string name="new_project_where">Where</string> <string name="new_project_where">Где</string>
<string name="where_local">Local only</string> <string name="where_local">Только локально</string>
<string name="todo_join">Join existing project</string> <string name="todo_join">Присоединиться к существующему проекту</string>
<string name="todo_create">Create new project</string> <string name="todo_create">Создать новый проект</string>
<string name="import_tooltip">Import from file</string> <string name="import_tooltip">Импортировать из файла</string>
<string name="choose_project_management_action">Project</string> <string name="choose_project_management_action">Проект</string>
<string name="project_added_success">Project added successfully.</string> <string name="project_added_success">Проект добавлен успешно.</string>
<string name="no_projects_text">You have no projects yet.</string> <string name="no_projects_text">У вас пока нет проектов.</string>
<string name="configure_account_choice">Configure Nextcloud account</string> <string name="configure_account_choice">Настроить учётную запись Nextcloud</string>
<string name="add_project_choice">Add project manually</string> <string name="add_project_choice">Добавить проект вручную</string>
<string name="no_members_text">No members in this project.</string> <string name="no_members_text">В этом проекте нет участников.</string>
<string name="no_bills_text">No bills found.</string> <string name="no_bills_text">Расходов не найдено.</string>
<string name="member_already_exists">Member already exists.</string> <string name="member_already_exists">Участник уже существует.</string>
<string name="activity_dialog_title">Project: %1$s</string> <string name="activity_dialog_title">Проект: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string> <string name="remove_project_confirmation">Проект %1$s удален.</string>
<string name="file_saved_success">File saved: %1$s</string> <string name="file_saved_success">Файл сохранен: %1$s</string>
<string name="import_error_header">Import failed at row %d</string> <string name="import_error_header">Ошибка импорта в строке %d</string>
<string name="import_error_date">Invalid date format at row %d</string> <string name="import_error_date">Недопустимый формат даты в строке %d</string>
<string name="import_error_owers">Invalid owers at row %d</string> <string name="import_error_owers">Недопустимые участники в строке %d</string>
<string name="add_member_dialog_title">Add Member</string> <string name="add_member_dialog_title">Добавить участника</string>
<string name="edit_member_dialog_title">Edit Member</string> <string name="edit_member_dialog_title">Редактировать участника</string>
<string name="member_edit_delete">Delete</string> <string name="member_edit_delete">Удалить</string>
<string name="project_edition_no_change">No changes to save.</string> <string name="project_edition_no_change">Изменений для сохранения нет.</string>
<!-- Settlement --> <!-- Settlement -->
<string name="center_none">None (Optimal)</string> <string name="center_none">Нет (оптимально)</string>
<string name="settle_who">Who pays</string> <string name="center_none_direct">Нет (напрямую)</string>
<string name="settle_to_whom">To whom</string> <string name="settle_who">Кто платит</string>
<string name="settle_how_much">Amount</string> <string name="settle_to_whom">Кому</string>
<string name="simple_settle_share">Share</string> <string name="settle_how_much">Сумма</string>
<string name="simple_create_bills">Create bills</string> <string name="simple_settle_share">Поделиться</string>
<string name="settle_bill_what">Settlement</string> <string name="simple_create_bills">Создать расходы</string>
<string name="settle_bill_what">Расход</string>
<!-- Currencies --> <!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string> <string name="currency_dialog_title">Выбрать валюту (%s)</string>
<string name="setting_none">None</string> <string name="setting_none">Нет</string>
<string name="setting_all">All</string> <string name="setting_all">Все</string>
<string name="currency_saved_success">Currency settings saved.</string> <string name="currency_saved_success">Настройки валюты сохранены.</string>
<string name="main_currency">Main Currency</string> <string name="main_currency">Основная валюта</string>
<!-- Statistics --> <!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string> <string name="label_bills_suggested">Рекомендуемые категории</string>
<string name="label_bills_skip">Skip</string> <string name="label_bills_skip">Пропустить</string>
<string name="stats_date_min">From</string> <string name="stats_date_min">С</string>
<string name="stats_date_max">To</string> <string name="stats_date_max">По</string>
<string name="stats_who">Member</string> <string name="stats_who">Участник</string>
<string name="stats_paid">Paid</string> <string name="stats_paid">Оплачено</string>
<string name="stats_spent">Spent</string> <string name="stats_spent">Потрачено</string>
<string name="stats_balance">Balance</string> <string name="stats_balance">Баланс</string>
<string name="total">Total: %1$s</string> <string name="total">Итого: %1$s</string>
<!-- Errors Extra --> <!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string> <string name="error_project_connect_check">Ошибка подключения: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string> <string name="error_create_remote_project_helper">Ошибка создания: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string> <string name="error_edit_remote_project_helper">Ошибка обновления удаленного проекта: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string> <string name="remote_project_operation_no_network">Отсутствует подключение к сети для операций на сервере.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string> <string name="error_scanning_bill_qr_code">Не удалось распознать QR-код.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string> <string name="error_token_mismatch">Ошибка токена аутентификации. Пожалуйста, войдите заново.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string> <string name="insufficient_access_level">У вас нет прав на выполнение этого действия.</string>
<string name="delete_label_confirmation_title">Delete Label</string> <string name="delete_label_confirmation_title">Удалить метку</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string> <string name="delete_label_confirmation_message">Вы действительно хотите удалить эту метку?</string>
<!-- About --> <!-- About -->
<string name="about_version">Version %1$s</string> <string name="about_version">Версия %1$s</string>
<string name="about_maintainer_title">Maintainer</string> <string name="about_maintainer_title">Сопровождающий</string>
<string name="about_license_title">License</string> <string name="about_license_title">Лицензия</string>
<string name="about_source_title">Source code</string> <string name="about_source_title">Исходный код</string>
<!-- New constants for backward compatibility or shared use --> <!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string> <string name="share_intent_title">Проект %1$s</string>
<string name="share_chooser_title">Share %1$s</string> <string name="share_chooser_title">Поделиться %1$s</string>
</resources> </resources>
+1
View File
@@ -247,6 +247,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>