Compare commits

..
Author SHA1 Message Date
Renovate Bot 7eee5fd2ae Update dependency androidx.compose.runtime:runtime to v1.12.0 2026-08-14 02:01:32 +00:00
choelzlandGitHub 1034e961ab New Crowdin updates (#6)
* New translations strings.xml (Romanian)

[ci skip]

* New translations strings.xml (French)

[ci skip]

* New translations strings.xml (Spanish)

[ci skip]

* New translations strings.xml (Afrikaans)

[ci skip]

* New translations strings.xml (Arabic)

[ci skip]

* New translations strings.xml (Catalan)

[ci skip]

* New translations strings.xml (Czech)

[ci skip]

* New translations strings.xml (Danish)

[ci skip]

* New translations strings.xml (German)

[ci skip]

* New translations strings.xml (Greek)

[ci skip]

* New translations strings.xml (Finnish)

[ci skip]

* New translations strings.xml (Hebrew)

[ci skip]

* New translations strings.xml (Hungarian)

[ci skip]

* New translations strings.xml (Italian)

[ci skip]

* New translations strings.xml (Japanese)

[ci skip]

* New translations strings.xml (Korean)

[ci skip]

* New translations strings.xml (Dutch)

[ci skip]

* New translations strings.xml (Norwegian)

[ci skip]

* New translations strings.xml (Polish)

[ci skip]

* New translations strings.xml (Portuguese)

[ci skip]

* New translations strings.xml (Russian)

[ci skip]

* New translations strings.xml (Serbian (Cyrillic))

[ci skip]

* New translations strings.xml (Swedish)

[ci skip]

* New translations strings.xml (Turkish)

[ci skip]

* New translations strings.xml (Ukrainian)

[ci skip]

* New translations strings.xml (Chinese Simplified)

[ci skip]

* New translations strings.xml (Chinese Traditional)

[ci skip]

* New translations strings.xml (English)

[ci skip]

* New translations strings.xml (Vietnamese)

[ci skip]

* New translations strings.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings.xml (Romanian)

[ci skip]

* New translations strings.xml (French)

[ci skip]

* New translations strings.xml (Spanish)

[ci skip]

* New translations strings.xml (Arabic)

[ci skip]

* New translations strings.xml (Czech)

[ci skip]

* New translations strings.xml (Danish)

[ci skip]

* New translations strings.xml (German)

[ci skip]

* New translations strings.xml (Greek)

[ci skip]

* New translations strings.xml (Finnish)

[ci skip]

* New translations strings.xml (Italian)

[ci skip]

* New translations strings.xml (Japanese)

[ci skip]

* New translations strings.xml (Dutch)

[ci skip]

* New translations strings.xml (Norwegian)

[ci skip]

* New translations strings.xml (Polish)

[ci skip]

* New translations strings.xml (Portuguese)

[ci skip]

* New translations strings.xml (Russian)

[ci skip]

* New translations strings.xml (Swedish)

[ci skip]

* New translations strings.xml (Ukrainian)

[ci skip]

* New translations strings.xml (Chinese Simplified)

[ci skip]

* New translations strings.xml (Portuguese, Brazilian)

[ci skip]
2026-08-03 20:20:30 +02:00
choelzl ff93fdd4d7 Update Crowdin configuration file
[ci skip]
2026-08-03 20:02:44 +02:00
soraefir 726c4a73dc Fix project creation nextcloud 2026-08-03 19:51:26 +02:00
soraefir 191f221faa Allow comma in title 2026-08-03 19:51:26 +02:00
sora 553e33d441 Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2026-07-12 14:47:56 +02:00
sora eec95cdcb0 Merge branch 'main' into renovate/configure 2026-07-12 14:47:47 +02:00
soraefir 106c4e27fc Fix sync and add beta features 2026-07-09 19:22:04 +02:00
soraefir 47d0471b48 Label sync and workflows 2026-07-05 15:02:36 +02:00
sora 56968dbe3b Update metadata/en-US/full_description.txt 2026-07-02 03:05:16 +02:00
soraefir 56036d5840 Fix toast & minor category/paymentmode fix 2026-07-02 02:43:33 +02:00
soraefir 4fa29da79d Better category/paymentmode storage and handling 2026-07-02 02:37:40 +02:00
sora c2c1017633 Update app/src/main/java/net/helcel/cowspent/model/DBPaymentMode.kt 2026-07-02 02:06:55 +02:00
sora 3aeabf4dd3 Update app/src/main/java/net/helcel/cowspent/model/DBCategory.kt 2026-07-02 02:06:41 +02:00
sora 937c3db1f5 Update app/src/main/java/net/helcel/cowspent/persistence/CowspentSQLiteOpenHelper.kt 2026-07-02 02:06:08 +02:00
sora 5b3c47045c Disable custom Label until fully implemented 2026-07-02 02:01:36 +02:00
soraefir 7581985abd Stripping fix 2026-07-01 22:12:42 +02:00
soraefir 810ca20201 Custom Labels, User archive, Versioning, String and UI cleanup 2026-07-01 22:10:08 +02:00
soraefir 678a90ead9 Archiving and Label options 2026-06-30 20:40:32 +02:00
soraefir 88469fa96c Fix monochrome logo 2026-06-30 19:31:39 +02:00
soraefir 119ae757b9 Migrate to SecureStorage 2026-06-30 19:24:25 +02:00
soraefir c3d380ddb4 Migrate to encryptedPref 2026-06-30 18:59:50 +02:00
soraefir a8ba00269b Hide inactive members in drawer 2026-06-30 07:41:34 +02:00
soraefir 756a37de23 String cleanup 2026-06-30 07:37:52 +02:00
soraefir 5f7266013d More cleanup 2026-06-30 07:33:49 +02:00
soraefir e98dc4a86b Cleanup imports and co 2026-06-30 07:30:57 +02:00
soraefir afb14ff7b9 Fix Monochrome 2026-06-30 07:26:33 +02:00
soraefir 1bee1dabbe Disable dependency tree, logo to png 2026-06-30 07:17:57 +02:00
soraefir 84432fe68d Better formating and placeholder 2026-06-29 21:38:01 +02:00
soraefir 7e00b14d27 Fix keyboard padding and better currency management 2026-06-29 21:31:59 +02:00
soraefir ec4d60897a Currency parsing and formating
# Conflicts:
#	app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt
2026-06-29 21:30:35 +02:00
soraefir 5c74336c10 Fixes for currency, logo, render and more 2026-06-27 12:18:33 +02:00
soraefir d617c5c7be project creation and currency db 2026-06-14 21:34:58 +02:00
soraefir a9a303478c better currency handling 2026-06-14 21:33:08 +02:00
sora 110ab397b8 Update README.md 2026-06-09 19:19:24 +02:00
sora 3675e87651 Upload files to ".github/images" 2026-06-09 19:18:02 +02:00
Renovate Bot c2f59fa1df Add renovate.json 2026-05-30 02:02:13 +00:00
115 changed files with 11701 additions and 2089 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+2 -1
View File
@@ -52,7 +52,8 @@ jobs:
- name: Build APK
run: |
VERSION_CODE=$(git rev-list --count HEAD)
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE
VERSION_NAME=$(git describe --tags --always)
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
- name: Release
uses: softprops/action-gh-release@v3
+2 -1
View File
@@ -1,9 +1,10 @@
<!--suppress ALL -->
<div align="center">
<h1>Cowspent</h1>
<img width="100px" src="./metadata/en-US/images/icon.webp" alt="Logo">
<img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo">
<p>
Shared budget manager able to sync with [Nextcloud Cospend](https://github.com/julien-nc/cospend-nc).
This work is based upon [MoneyBuster](https://gitlab.com/eneiluj/moneybuster).
+19 -5
View File
@@ -14,7 +14,7 @@ android {
applicationId "net.helcel.cowspent"
minSdk = 26
targetSdk = 37
versionName "1.0b"
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
}
@@ -82,11 +82,22 @@ android {
androidResources {
generateLocaleConfig = true
}
packaging {
jniLibs.keepDebugSymbols.add("**/*.so")
}
dependenciesInfo {
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
includeInApk = false
// Disables dependency metadata when building Android App Bundles (for Google Play)
includeInBundle = false
}
}
dependencies {
implementation 'androidx.compose.foundation:foundation:1.11.2'
implementation 'androidx.compose.runtime:runtime:1.11.2'
implementation 'androidx.compose.foundation:foundation:1.11.4'
implementation 'androidx.compose.runtime:runtime:1.12.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'androidx.preference:preference-ktx:1.2.1'
@@ -99,7 +110,7 @@ dependencies {
implementation 'com.github.nextcloud:Android-SingleSignOn:1.1.0'
implementation 'com.opencsv:opencsv:5.12.0'
implementation platform('androidx.compose:compose-bom:2026.05.01')
implementation platform('androidx.compose:compose-bom:2026.06.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material:material'
implementation 'androidx.compose.material:material-icons-extended'
@@ -107,5 +118,8 @@ dependencies {
debugImplementation 'androidx.compose.ui:ui-tooling'
implementation 'androidx.activity:activity-compose:1.13.0'
implementation 'androidx.activity:activity-ktx:1.13.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0'
implementation 'androidx.security:security-crypto:1.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0'
implementation "androidx.datastore:datastore-preferences:1.2.1"
implementation "com.google.crypto.tink:tink-android:1.22.0"
}
+19 -7
View File
@@ -46,7 +46,7 @@
<activity
android:name="net.helcel.cowspent.android.account.AccountActivity"
android:label="@string/settings_server_settings"
android:label="@string/title_account"
android:parentActivityName="net.helcel.cowspent.android.settings.PreferencesActivity"
android:windowSoftInputMode="adjustResize|stateVisible"
android:launchMode="standard"
@@ -67,7 +67,7 @@
<activity
android:name="net.helcel.cowspent.android.project.create.NewProjectActivity"
android:label="@string/simple_add_project"
android:label="@string/title_add_project"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="standard"
@@ -94,7 +94,7 @@
<activity
android:name="net.helcel.cowspent.android.project.edit.EditProjectActivity"
android:label="@string/simple_edit_project"
android:label="@string/title_edit_project"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="standard">
@@ -102,7 +102,7 @@
<activity
android:name="net.helcel.cowspent.android.bill_edit.EditBillActivity"
android:label="@string/simple_edit_bill"
android:label="@string/action_edit"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:windowSoftInputMode="adjustResize"
android:launchMode="standard">
@@ -110,20 +110,32 @@
<activity
android:name="net.helcel.cowspent.android.about.AboutActivity"
android:label="@string/simple_about"
android:label="@string/title_about"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity" />
<activity
android:name="net.helcel.cowspent.android.statistics.ProjectStatisticsActivity"
android:label="@string/statistic_title"
android:label="@string/title_stats"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name="net.helcel.cowspent.android.bill_label.LabelBillsActivity"
android:label="@string/label_bills_title"
android:label="@string/title_label_bills"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name="net.helcel.cowspent.android.label.LabelManagementActivity"
android:label="@string/title_labels"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="net.helcel.cowspent.android.project.member.MemberManagementActivity"
android:label="@string/action_members"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>
@@ -45,7 +45,7 @@ fun AboutScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.simple_about)) },
title = { Text(stringResource(R.string.title_about)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -13,8 +13,8 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
@@ -33,7 +33,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.nextcloud.android.sso.AccountImporter
import com.nextcloud.android.sso.helper.SingleAccountHelper
import com.nextcloud.android.sso.model.SingleSignOnAccount
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -42,6 +41,7 @@ import net.helcel.cowspent.android.main.MainConstants
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.CospendClientUtil
import net.helcel.cowspent.util.CospendClientUtil.LoginStatus
import net.helcel.cowspent.util.SecureStorage
import java.net.URLDecoder
import java.util.Locale
@@ -69,7 +69,6 @@ class AccountActivity : AppCompatActivity() {
}
private lateinit var preferences: SharedPreferences
private var oldPassword = ""
private var useWebLogin = true
private var showLoginDialog by mutableStateOf(false)
@@ -129,8 +128,6 @@ class AccountActivity : AppCompatActivity() {
)
}
}
oldPassword = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS) ?: ""
viewModel.validateUrl()
}
@@ -224,11 +221,7 @@ class AccountActivity : AppCompatActivity() {
private fun legacyLogin() {
val url = CospendClientUtil.formatURL(viewModel.serverUrl.trim())
val username = viewModel.username
var password = viewModel.password
if (password.isEmpty()) {
password = oldPassword
}
val password = viewModel.password
performLogin(url, username, password)
}
@@ -311,10 +304,10 @@ class AccountActivity : AppCompatActivity() {
}
if (status == LoginStatus.OK) {
SecureStorage.savePassword(applicationContext, SETTINGS_PASSWORD, password)
preferences.edit {
putString(SETTINGS_URL, url)
putString(SETTINGS_USERNAME, username)
putString(SETTINGS_PASSWORD, password)
remove(SETTINGS_KEY_ETAG)
remove(SETTINGS_KEY_LAST_MODIFIED)
}
@@ -4,39 +4,18 @@
package net.helcel.cowspent.android.account
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
@@ -99,7 +78,7 @@ fun AccountScreenContent(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.settings_server_settings)) },
title = { Text(stringResource(R.string.title_account)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -123,30 +102,70 @@ fun AccountScreenContent(
}
} else if (isLoggedIn) {
Text(
text = stringResource(R.string.account_logged_in_as, username),
style = MaterialTheme.typography.h6
)
Text(
text = serverUrl,
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
text = "CURRENT ACCOUNT",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(32.dp))
Column {
Text(
text = stringResource(R.string.msg_logged_in_as, username),
style = MaterialTheme.typography.subtitle1
)
Text(
text = serverUrl,
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onLogout,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.account_logout))
Text(stringResource(R.string.action_logout))
}
Spacer(modifier = Modifier.height(24.dp))
}
if (!isValidatingLogin) {
Text(
text = "CONNECTION SETTINGS",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp, top = if (isLoggedIn) 16.dp else 0.dp)
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
Text(stringResource(R.string.use_sso_toggle), modifier = Modifier.weight(1f))
Icon(
Icons.Default.Sync,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(32.dp))
Text(stringResource(R.string.label_use_sso), modifier = Modifier.weight(1f), style = MaterialTheme.typography.subtitle1)
Switch(
checked = useSso,
onCheckedChange = { onSsoClick(it) },
@@ -163,7 +182,7 @@ fun AccountScreenContent(
OutlinedTextField(
value = serverUrl,
onValueChange = onServerUrlChange,
label = { Text(stringResource(R.string.settings_url)) },
placeholder = { Text(stringResource(R.string.label_url)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Link, contentDescription = null) },
trailingIcon = {
@@ -192,7 +211,7 @@ fun AccountScreenContent(
OutlinedTextField(
value = username,
onValueChange = onUsernameChange,
label = { Text(stringResource(R.string.settings_username)) },
placeholder = { Text(stringResource(R.string.label_username)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) },
singleLine = true
@@ -204,7 +223,7 @@ fun AccountScreenContent(
OutlinedTextField(
value = password,
onValueChange = onPasswordChange,
label = { Text(stringResource(R.string.settings_password)) },
placeholder = { Text(stringResource(R.string.label_password)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) },
visualTransformation = PasswordVisualTransformation(),
@@ -221,7 +240,7 @@ fun AccountScreenContent(
if (isSubmitting) {
CustomCircularProgressIndicator(size = 24.dp, color = Color.White)
} else {
Text(stringResource(R.string.settings_submit))
Text(stringResource(R.string.action_connect))
}
}
}
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.cowspent.util.CospendClientUtil
import net.helcel.cowspent.util.SecureStorage
class AccountViewModel(application: Application) : AndroidViewModel(application) {
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
@@ -60,14 +61,15 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
} else {
preferences.getString(AccountActivity.SETTINGS_USERNAME, "")
}
val password = if (useSso) {
""
} else {
preferences.getString(AccountActivity.SETTINGS_PASSWORD, "")
}
if (!url.isNullOrEmpty() && !username.isNullOrEmpty()) {
viewModelScope.launch {
val password = if (useSso) {
""
} else {
SecureStorage.getPassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
}
isValidatingLogin = true
isLoggedIn = withContext(Dispatchers.IO) {
if (useSso) {
@@ -87,6 +89,9 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
}
fun logout() {
viewModelScope.launch {
SecureStorage.removePassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
}
preferences.edit {
remove(AccountActivity.SETTINGS_USE_SSO)
remove(AccountActivity.SETTINGS_SSO_URL)
@@ -15,6 +15,7 @@ import androidx.compose.material.TextButton
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -65,8 +66,10 @@ fun LoginDialogContent(
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = "Login",
style = MaterialTheme.typography.h6,
text = "LOGIN",
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 16.dp)
)
@@ -26,7 +26,7 @@ import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillParser
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil
import java.text.ParseException
import java.time.ZoneId
import java.util.Calendar
@@ -51,23 +51,10 @@ class EditBillActivity : AppCompatActivity() {
ThemeUtils.CowspentTheme {
val categories = remember {
val syncedCategories = db.getCategories(bill.projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@EditBillActivity, bill.projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
syncedCategories + hardcoded
db.getCategories(bill.projectId)
}
val paymentModes = remember {
val syncedPaymentModes = db.getPaymentModes(bill.projectId)
val defaultPaymentModes = CategoryUtils.getDefaultPaymentModes(this@EditBillActivity, bill.projectId)
if (projectType == ProjectType.LOCAL) {
syncedPaymentModes + defaultPaymentModes
} else {
syncedPaymentModes.ifEmpty { defaultPaymentModes }
}
db.getPaymentModes(bill.projectId)
}
EditBillScreen(
@@ -107,7 +94,8 @@ class EditBillActivity : AppCompatActivity() {
val createIntent = Intent(this@EditBillActivity, QrCodeScannerActivity::class.java)
scanQRCodeLauncher.launch(createIntent)
},
onDelete = if (bill.id > 0) { { deleteBillAsked() } } else null,
onDuplicate = if (!viewModel.isNewBill) { { duplicateCurrentBill() } } else null,
onDelete = if (!viewModel.isNewBill) { { deleteBillAsked() } } else null,
accessLevel = db.getProject(bill.projectId)?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN
)
}
@@ -142,7 +130,7 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill(
first.id, 0, first.projectId, first.payerId, totalAmount,
first.timestamp, first.what, first.state, first.repeat,
first.paymentMode, first.categoryRemoteId, first.comment, first.paymentModeRemoteId
first.paymentMode, first.categoryId, first.comment, first.paymentModeId
)
val splits = mutableMapOf<Long, Double>()
@@ -173,8 +161,8 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill(
0, 0, projectId, btd.payerId, btd.amount,
timeNowSeconds, btd.what, DBBill.STATE_ADDED,
btd.repeat, btd.paymentMode, btd.categoryRemoteId,
btd.comment, btd.paymentModeRemoteId
btd.repeat, btd.paymentMode, btd.categoryId,
btd.comment, btd.paymentModeId
)
val btdOwers = btd.billOwers
val newBillOwers = btdOwers.filter {
@@ -185,12 +173,23 @@ class EditBillActivity : AppCompatActivity() {
}
}
calendar.timeInMillis = bill.timestamp * 1000
val members = db.getMembersOfProject(bill.projectId, null)
val allMembers = db.getMembersOfProject(bill.projectId, null)
val billOwerIds = bill.billOwersIds
val members = allMembers.filter { m ->
m.isActivated || m.id == bill.payerId || billOwerIds.contains(m.id)
}
val project = db.getProject(bill.projectId)
val currencies = db.getCurrencies(bill.projectId)
if (project != null) {
db.ensureDefaultLabels(bill.projectId, project.type)
}
withContext(Dispatchers.Main) {
viewModel.currencies = currencies
viewModel.mainCurrencyName = project?.currencyName ?: ""
viewModel.isNewBill = (bill.id == 0L)
viewModel.initFromBill(bill, members, customSplits)
}
}
@@ -206,6 +205,7 @@ class EditBillActivity : AppCompatActivity() {
calendar.timeInMillis = austrianBill.date.time
viewModel.timestamp = calendar.timeInMillis / 1000
viewModel.amount = austrianBill.amount.toString()
viewModel.updateSplits()
return@registerForActivityResult
} catch (_: ParseException) {
}
@@ -217,6 +217,7 @@ class EditBillActivity : AppCompatActivity() {
viewModel.timestamp = calendar.timeInMillis / 1000
}
viewModel.amount = croatianBill.amount.toString()
viewModel.updateSplits()
return@registerForActivityResult
} catch (_: ParseException) {
}
@@ -225,28 +226,42 @@ class EditBillActivity : AppCompatActivity() {
}
}
private fun duplicateCurrentBill() {
bill = DBBill(
0, 0, bill.projectId, viewModel.payerId, viewModel.amountAsDouble,
System.currentTimeMillis() / 1000, viewModel.what, DBBill.STATE_ADDED,
viewModel.repeat, bill.paymentMode, viewModel.categoryId,
viewModel.getFinalComment(), viewModel.paymentModeId
)
calendar.timeInMillis = System.currentTimeMillis()
viewModel.timestamp = calendar.timeInMillis / 1000
viewModel.isNewBill = true
showToast(this, "Duplicating bill...")
}
private fun onBack() {
if (!valuesHaveChanged()) {
finish()
return
}
viewModel.showDialog(
title = getString(R.string.save_or_discard_bill_dialog_title),
message = getString(R.string.save_or_discard_bill_dialog_message),
positiveText = getString(R.string.save_or_discard_bill_dialog_save),
title = getString(R.string.dialog_unsaved_changes_title),
message = getString(R.string.dialog_unsaved_changes_msg),
positiveText = getString(R.string.action_save),
onConfirm = { saveBillAsked() },
negativeText = getString(R.string.save_or_discard_bill_dialog_discard),
negativeText = getString(R.string.action_discard),
onCancel = { finish() }
)
}
private fun saveBillAsked() {
val validationError = viewModel.getValidationError(
getString(R.string.error_invalid_bill_what),
getString(R.string.error_invalid_bill_name),
getString(R.string.error_invalid_bill_date),
getString(R.string.error_invalid_bill_payerid),
getString(R.string.error_invalid_bill_payer),
getString(R.string.error_invalid_bill_owers),
getString(R.string.simple_error)
getString(R.string.error_generic)
)
if (validationError != null) {
@@ -264,7 +279,7 @@ class EditBillActivity : AppCompatActivity() {
private fun deleteBillAsked() {
viewModel.showDialog(
title = getString(R.string.confirm_remove_project_dialog_title),
title = getString(R.string.title_confirm),
message = bill.what,
positiveText = getString(R.string.action_delete),
onConfirm = {
@@ -298,12 +313,12 @@ class EditBillActivity : AppCompatActivity() {
return !(bill.what == viewModel.what &&
bill.timestamp == viewModel.timestamp &&
bill.amount == viewModel.amountAsDouble &&
bill.amount == viewModel.getFinalAmount() &&
bill.payerId == viewModel.payerId &&
bill.comment == viewModel.comment &&
bill.comment == viewModel.getFinalComment() &&
bill.repeat == viewModel.repeat &&
bill.categoryRemoteId == viewModel.categoryRemoteId &&
bill.paymentModeRemoteId == viewModel.paymentModeRemoteId &&
bill.categoryId == viewModel.categoryId &&
bill.paymentModeId == viewModel.paymentModeId &&
!owersChanged)
}
@@ -312,13 +327,17 @@ class EditBillActivity : AppCompatActivity() {
val isCustomSplit = viewModel.isCustomSplit
if (isCustomSplit) {
val splits = viewModel.owersCustomSplit.filter { (id, amount) ->
viewModel.owersSelection[id] == true && (amount.replace(',', '.').toDoubleOrNull()
val splits: Map<Long, Double> = viewModel.owersCustomSplit.filter { (id, amountStr) ->
viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull()
?: 0.0) > 0
}.mapValues { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 }
}.mapValues {
val uiAmount = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
}
if (splits.isEmpty()) return@withContext 0L
val finalComment = viewModel.getFinalComment()
val splitEntries = splits.entries.toList()
// Pool of existing bills in this group that we can potentially reuse
@@ -358,9 +377,9 @@ class EditBillActivity : AppCompatActivity() {
listOf(memberId),
viewModel.repeat,
existingBill.paymentMode,
viewModel.paymentModeRemoteId,
viewModel.categoryRemoteId,
viewModel.comment
viewModel.paymentModeId,
viewModel.categoryId,
finalComment
)
if (firstSavedId == 0L) firstSavedId = billToUseId
} else {
@@ -368,9 +387,11 @@ class EditBillActivity : AppCompatActivity() {
val newBill = DBBill(
0, 0, bill.projectId, viewModel.payerId, amount,
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
bill.paymentMode, viewModel.categoryRemoteId, viewModel.comment, viewModel.paymentModeRemoteId
bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
)
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
newBill.categoryId = viewModel.categoryId
newBill.paymentModeId = viewModel.paymentModeId
val newId = db.addBill(newBill)
if (firstSavedId == 0L) firstSavedId = newId
}
@@ -398,7 +419,8 @@ class EditBillActivity : AppCompatActivity() {
return@withContext firstSavedId
} else {
val newAmount = viewModel.amountAsDouble
val newAmount = viewModel.getFinalAmount()
val finalComment = viewModel.getFinalComment()
val newOwersIds = viewModel.getOwersIds()
if (bill.id != 0L) {
@@ -412,9 +434,9 @@ class EditBillActivity : AppCompatActivity() {
newOwersIds,
viewModel.repeat,
bill.paymentMode,
viewModel.paymentModeRemoteId,
viewModel.categoryRemoteId,
viewModel.comment
viewModel.paymentModeId,
viewModel.categoryId,
finalComment
)
if (groupedBillIds != null) {
for (id in groupedBillIds) {
@@ -431,7 +453,7 @@ class EditBillActivity : AppCompatActivity() {
val newBill = DBBill(
0, 0, bill.projectId, viewModel.payerId, newAmount,
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
bill.paymentMode, viewModel.categoryRemoteId, viewModel.comment, viewModel.paymentModeRemoteId
bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
)
newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) }
val newBillId = db.addBill(newBill)
@@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
@@ -30,6 +31,7 @@ import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.*
import net.helcel.cowspent.model.*
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil
import java.util.Date
import kotlin.math.abs
@@ -45,6 +47,7 @@ fun EditBillScreen(
onDateClick: () -> Unit,
onTimeClick: () -> Unit,
onScan: () -> Unit,
onDuplicate: (() -> Unit)? = null,
onDelete: (() -> Unit)? = null,
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN
) {
@@ -59,7 +62,7 @@ fun EditBillScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(if (viewModel.what.isEmpty()) R.string.simple_new_bill else R.string.simple_edit_bill)) },
title = { Text(stringResource(if (viewModel.isNewBill) R.string.action_new_bill else R.string.action_edit)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -70,6 +73,11 @@ fun EditBillScreen(
IconButton(onClick = onScan) {
Icon(Icons.Default.QrCodeScanner, contentDescription = null)
}
if (onDuplicate != null) {
IconButton(onClick = onDuplicate) {
Icon(Icons.Default.ContentCopy, contentDescription = "Duplicate")
}
}
if (onDelete != null) {
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete))
@@ -83,11 +91,11 @@ fun EditBillScreen(
},
floatingActionButton = {
if (canEdit) {
val errorWhat = stringResource(R.string.error_invalid_bill_what)
val errorWhat = stringResource(R.string.error_invalid_bill_name)
val errorDate = stringResource(R.string.error_invalid_bill_date)
val errorPayer = stringResource(R.string.error_invalid_bill_payerid)
val errorPayer = stringResource(R.string.error_invalid_bill_payer)
val errorOwers = stringResource(R.string.error_invalid_bill_owers)
val errorInvalidForm = stringResource(R.string.simple_error)
val errorInvalidForm = stringResource(R.string.error_generic)
FloatingActionButton(onClick = {
val validationError = viewModel.getValidationError(
@@ -101,7 +109,7 @@ fun EditBillScreen(
}) {
Icon(
Icons.Default.Done,
contentDescription = stringResource(R.string.action_save_bill)
contentDescription = stringResource(R.string.action_save)
)
}
}
@@ -111,6 +119,7 @@ fun EditBillScreen(
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.fillMaxSize()
.verticalScroll(scrollState)
@@ -157,16 +166,23 @@ fun BillBasicInfoSection(
onDateClick: () -> Unit,
onTimeClick: () -> Unit
) {
Text(
text = "GENERAL",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
val context = LocalContext.current
val currencyDialogTitle =
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
val noCurrencyError = stringResource(R.string.no_currency_error)
OutlinedTextField(
value = viewModel.what,
onValueChange = { viewModel.what = it },
enabled = canEdit,
placeholder = { Text(stringResource(R.string.setting_what)) },
placeholder = { Text(stringResource(R.string.label_what)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
)
@@ -182,25 +198,37 @@ fun BillBasicInfoSection(
enabled = canEdit,
placeholder = { Text("0") },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.AttachMoney, contentDescription = null) },
leadingIcon = {
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
viewModel.mainCurrencyName.ifEmpty { "$" }
}
TextIconDisplay(
textIcon = TextIcon.Symbol(currencyToShow),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
},
trailingIcon = {
IconButton(
enabled = canEdit,
onClick = {
if (viewModel.currencies.isNotEmpty()) {
viewModel.showDialog(
title = currencyDialogTitle,
items = viewModel.currencies.map { "${it.name} (${it.exchangeRate})" },
onItemSelected = { index ->
viewModel.convertCurrency(viewModel.currencies[index])
}
)
} else {
showToast(context, noCurrencyError)
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" }
val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
}
viewModel.showDialog(
title = currencyDialogTitle,
items = options,
onItemSelected = { index ->
if (index == 0) {
viewModel.resetCurrency()
} else {
viewModel.convertCurrency(viewModel.currencies[index - 1])
}
}
)
}
) {
Icon(Icons.Default.CurrencyExchange, contentDescription = null)
Icon(Icons.Default.SwapHoriz, contentDescription = "Change Currency")
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
@@ -221,7 +249,7 @@ fun BillBasicInfoSection(
onClick = onDateClick,
modifier = Modifier.weight(1f),
enabled = canEdit,
leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }
leadingIcon = { Icon(Icons.Default.Event, contentDescription = "Select Date") }
)
Spacer(modifier = Modifier.width(8.dp))
ClickableOutlinedTextField(
@@ -229,7 +257,7 @@ fun BillBasicInfoSection(
onClick = onTimeClick,
modifier = Modifier.weight(1f),
enabled = canEdit,
leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = null) }
leadingIcon = { Icon(Icons.Default.AccessTime, contentDescription = "Select Time") }
)
}
}
@@ -244,20 +272,16 @@ fun PayerSection(
EditableExposedDropdownMenu(
value = selectedPayer?.name ?: "",
placeholder = stringResource(R.string.setting_payer),
placeholder = stringResource(R.string.label_payer),
expanded = payerExpanded,
onExpandedChange = { payerExpanded = it },
onDismissRequest = { payerExpanded = false },
enabled = canEdit,
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
Box(modifier = Modifier) {
if (selectedPayer != null) {
UserAvatar(
name = selectedPayer.name,
r = selectedPayer.r,
g = selectedPayer.g,
b = selectedPayer.b,
disabled = !selectedPayer.isActivated,
MemberAvatar(
member = selectedPayer,
size = 24.dp
)
} else {
@@ -271,12 +295,8 @@ fun PayerSection(
viewModel.payerId = member.id
payerExpanded = false
}) {
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 24.dp
)
Spacer(modifier = Modifier.width(8.dp))
@@ -330,8 +350,10 @@ fun OwerSelectionSection(
}
Spacer(modifier = Modifier.width(8.dp))
Text(
stringResource(R.string.setting_owers), fontSize = 12.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
text = stringResource(R.string.label_owers).uppercase(),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold
)
Spacer(Modifier.weight(1f))
@@ -381,12 +403,8 @@ fun OwerSelectionSection(
enabled = canEdit,
onCheckedChange = { viewModel.toggleMember(member.id, it) }
)
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 32.dp
)
Spacer(modifier = Modifier.width(8.dp))
@@ -441,19 +459,28 @@ fun BillAdditionalDetailsSection(
paymentModes: List<DBPaymentMode>,
canEdit: Boolean
) {
Text(
text = "DETAILS",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp, top = 16.dp)
)
val context = LocalContext.current
var categoryExpanded by remember { mutableStateOf(false) }
val selectedCategory =
categories.find { it.remoteId.toInt() == viewModel.categoryRemoteId }
categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(context, viewModel.categoryId)
EditableExposedDropdownMenu(
value = selectedCategory?.name ?: "",
placeholder = stringResource(R.string.setting_category),
placeholder = stringResource(R.string.label_category),
expanded = categoryExpanded,
onExpandedChange = { categoryExpanded = it },
onDismissRequest = { categoryExpanded = false },
enabled = canEdit,
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
Box(modifier = Modifier) {
if (selectedCategory != null) {
Text(text = selectedCategory.icon, fontSize = 20.sp)
} else {
@@ -463,16 +490,26 @@ fun BillAdditionalDetailsSection(
},
content = {
DropdownMenuItem(onClick = {
viewModel.categoryRemoteId = 0
viewModel.categoryId = 0
categoryExpanded = false
}) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_none))
}
categories.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT
categoryExpanded = false
}) {
Text(text = "\uD83D\uDCB0", fontSize = 20.sp)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_reimbursement))
}
categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryRemoteId = category.remoteId.toInt()
viewModel.categoryId = category.id
categoryExpanded = false
}) {
Text(text = category.icon, fontSize = 20.sp)
@@ -487,17 +524,17 @@ fun BillAdditionalDetailsSection(
var pmExpanded by remember { mutableStateOf(false) }
val selectedPm =
paymentModes.find { it.remoteId.toInt() == viewModel.paymentModeRemoteId }
paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(context, viewModel.paymentModeId)
EditableExposedDropdownMenu(
value = selectedPm?.name ?: "",
placeholder = stringResource(R.string.setting_payment_mode),
placeholder = stringResource(R.string.label_mode),
expanded = pmExpanded,
onExpandedChange = { pmExpanded = it },
onDismissRequest = { pmExpanded = false },
enabled = canEdit,
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
Box(modifier = Modifier) {
if (selectedPm != null) {
Text(text = selectedPm.icon, fontSize = 20.sp)
} else {
@@ -507,7 +544,7 @@ fun BillAdditionalDetailsSection(
},
content = {
DropdownMenuItem(onClick = {
viewModel.paymentModeRemoteId = 0
viewModel.paymentModeId = 0
pmExpanded = false
}) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
@@ -516,7 +553,7 @@ fun BillAdditionalDetailsSection(
}
paymentModes.forEach { pm ->
DropdownMenuItem(onClick = {
viewModel.paymentModeRemoteId = pm.remoteId.toInt()
viewModel.paymentModeId = pm.id
pmExpanded = false
}) {
Text(text = pm.icon, fontSize = 20.sp)
@@ -542,7 +579,7 @@ fun BillAdditionalDetailsSection(
EditableExposedDropdownMenu(
value = selectedRepeat?.second ?: "",
placeholder = stringResource(R.string.setting_project_repetition),
placeholder = stringResource(R.string.label_repeat),
expanded = repeatExpanded,
onExpandedChange = { repeatExpanded = it },
onDismissRequest = { repeatExpanded = false },
@@ -566,7 +603,7 @@ fun BillAdditionalDetailsSection(
value = viewModel.comment,
onValueChange = { viewModel.comment = it },
enabled = canEdit,
placeholder = { Text(stringResource(R.string.setting_comment)) },
placeholder = { Text(stringResource(R.string.label_comment)) },
modifier = Modifier.fillMaxWidth(),
singleLine = false,
leadingIcon = {
@@ -587,6 +624,7 @@ fun EditBillScreenPreview() {
viewModel = EditBillViewModel().apply {
what = "Pizza"
amount = "12.50"
mainCurrencyName = "EUR"
members = listOf(
DBMember(1, 0, 0, "Alice", true, 1.0, 0, null, null, null, null, null),
DBMember(2, 0, 0, "Bob", true, 1.0, 0, null, null, null, null, null)
@@ -1,21 +1,21 @@
package net.helcel.cowspent.android.bill_edit
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.android.helper.parseAmount
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath
import net.helcel.cowspent.model.DBCurrency
import androidx.compose.ui.graphics.vector.ImageVector
class EditBillViewModel : ViewModel() {
var what by mutableStateOf("")
var amount by mutableStateOf("")
@@ -23,11 +23,14 @@ class EditBillViewModel : ViewModel() {
var timestamp by mutableLongStateOf(0L)
var payerId by mutableLongStateOf(0L)
var repeat by mutableStateOf(DBBill.NON_REPEATED)
var paymentModeRemoteId by mutableIntStateOf(0)
var categoryRemoteId by mutableIntStateOf(0)
var paymentModeId by mutableLongStateOf(0L)
var categoryId by mutableLongStateOf(0L)
var isNewBill by mutableStateOf(false)
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
var mainCurrencyName by mutableStateOf("")
var selectedCurrencyName by mutableStateOf("")
var selectedCurrencyRate by mutableDoubleStateOf(1.0)
var members by mutableStateOf<List<DBMember>>(emptyList())
var owersSelection = mutableStateMapOf<Long, Boolean>()
@@ -38,13 +41,8 @@ class EditBillViewModel : ViewModel() {
val amountAsDouble: Double
get() {
val amountStr = amount.replace(',', '.')
return try {
if (amountStr.matches("[0-9.]+".toRegex())) {
amountStr.toDouble()
} else {
evalMath(amountStr)
}
return parseAmount(amount) ?: try {
evalMath(amount.replace(',', '.'))
} catch (_: Exception) {
0.0
}
@@ -128,46 +126,81 @@ class EditBillViewModel : ViewModel() {
}
fun convertCurrency(currency: DBCurrency) {
val originalAmountStr = amount
val originalAmount = amountAsDouble
if (originalAmount == 0.0) return
selectedCurrencyName = currency.name ?: ""
selectedCurrencyRate = currency.exchangeRate
val newAmount = originalAmount / currency.exchangeRate
amount = SupportUtil.round2(newAmount).toString()
val currencyLabel = currency.name ?: ""
val conversionNote = "($originalAmountStr $currencyLabel)"
if (!comment.contains(conversionNote)) {
if (comment.isNotEmpty() && !comment.endsWith(" ")) {
comment += " "
}
comment += conversionNote
}
if (isCustomSplit) {
owersCustomSplit.keys.toList().forEach { id ->
val value = owersCustomSplit[id] ?: ""
val partAmount = value.replace(',', '.').toDoubleOrNull() ?: 0.0
if (partAmount != 0.0) {
val newPartAmount = partAmount / currency.exchangeRate
owersCustomSplit[id] = SupportUtil.round2(newPartAmount).toString()
}
}
}
// We don't change 'amount' here anymore as per request.
// It stays as the original amount.
updateSplits()
}
fun resetCurrency() {
selectedCurrencyName = ""
selectedCurrencyRate = 1.0
updateSplits()
}
fun getFinalAmount(): Double {
return SupportUtil.round2(amountAsDouble / selectedCurrencyRate)
}
fun getFinalComment(): String {
var baseComment = comment
val regex = "\\s*#[^:]+:[\\d.,]+@[\\d.,]+".toRegex()
baseComment = baseComment.replace(regex, "").trim()
return if (selectedCurrencyName.isNotEmpty() && selectedCurrencyRate != 1.0) {
val metadata = "#$selectedCurrencyName:$amount@$selectedCurrencyRate"
if (baseComment.isEmpty()) metadata else "$baseComment $metadata"
} else {
baseComment
}
}
fun initFromBill(bill: DBBill, members: List<DBMember>, customSplits: Map<Long, Double>? = null) {
this.members = members
what = bill.what
amount = if (bill.amount == 0.0) "" else bill.amount.toString()
comment = bill.comment ?: ""
timestamp = bill.timestamp
payerId = bill.payerId
repeat = bill.repeat ?: DBBill.NON_REPEATED
paymentModeRemoteId = bill.paymentModeRemoteId
categoryRemoteId = bill.categoryRemoteId
paymentModeId = bill.paymentModeId
categoryId = bill.categoryId
val rawComment = bill.comment ?: ""
// Try to parse existing conversion metadata #CURR:ORIG@RATE
// Using [^:]+ for currency name to support symbols and names with spaces
val regex = "#([^:]+):([\\d.,]+)@([\\d.,]+)".toRegex()
val match = regex.find(rawComment)
if (match != null) {
val (currName, origAmount, rate) = match.destructured
selectedCurrencyName = currName
selectedCurrencyRate = rate.replace(',', '.').toDoubleOrNull() ?: 1.0
amount = origAmount
comment = rawComment.replace(match.value, "").trim()
// Check if latest rate is different
val latestCurrency = currencies.find { it.name == selectedCurrencyName }
if (latestCurrency != null && latestCurrency.exchangeRate != selectedCurrencyRate) {
showDialog(
title = "Update Exchange Rate?",
message = "The exchange rate for $selectedCurrencyName has changed from $selectedCurrencyRate to ${latestCurrency.exchangeRate}. Do you want to update the conversion for the saved total?",
positiveText = "Update Rate",
negativeText = "Keep Old",
onConfirm = {
selectedCurrencyRate = latestCurrency.exchangeRate
updateSplits()
}
)
}
} else {
selectedCurrencyName = ""
selectedCurrencyRate = 1.0
amount = if (bill.amount == 0.0) "" else bill.amount.toString()
comment = rawComment
}
owersSelection.clear()
owersCustomSplit.clear()
@@ -178,13 +211,23 @@ class EditBillViewModel : ViewModel() {
val selected = customSplits.containsKey(member.id)
owersSelection[member.id] = selected
if (selected) {
owersCustomSplit[member.id] = SupportUtil.round2(customSplits[member.id]!!).toString()
// If we have metadata, the custom splits from DB are also converted.
// We should show them as "Original" if possible?
// Actually, if we use metadata, we should probably store original splits too,
// but for now let's just reverse the rate for display.
val dbPart = customSplits[member.id]!!
val uiPart = if (selectedCurrencyRate != 1.0) dbPart * selectedCurrencyRate else dbPart
owersCustomSplit[member.id] = SupportUtil.round2(uiPart).toString()
}
}
} else {
// Even split logic
val billOwerIds = bill.billOwersIds
val selectedCount = billOwerIds.size
val evenSplit = if (selectedCount > 0) bill.amount / selectedCount else 0.0
// Use UI amount for even split calculation
val uiAmount = amountAsDouble
val evenSplit = if (selectedCount > 0) uiAmount / selectedCount else 0.0
val evenSplitStr = if (evenSplit == 0.0) "" else SupportUtil.round2(evenSplit).toString()
for (member in members) {
@@ -198,7 +241,7 @@ class EditBillViewModel : ViewModel() {
}
fun isFormValid(): Boolean {
return what.isNotEmpty() && !what.contains(",") &&
return what.isNotEmpty() &&
timestamp != 0L &&
payerId != 0L &&
owersSelection.any { it.value }
@@ -212,7 +255,7 @@ class EditBillViewModel : ViewModel() {
errorInvalidForm: String
): String? {
return when {
what.isEmpty() || what.contains(",") -> errorWhat
what.isEmpty() -> errorWhat
timestamp == 0L -> errorDate
payerId == 0L -> errorPayer
owersSelection.none { it.value } -> errorOwers
@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.ProjectType
@@ -39,24 +38,20 @@ class LabelBillsActivity : AppCompatActivity() {
val members = db.getMembersOfProject(projectId, null)
val allBills = db.getBillsOfProject(projectId)
val billsToLabel = allBills.filter { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED }
val allCategorized = allBills.filter { it.categoryRemoteId != 0 && it.state != DBBill.STATE_DELETED }
val billsToLabel = allBills.filter { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val allCategorized = allBills.filter { it.categoryId != 0L && it.state != DBBill.STATE_DELETED }
val syncedCategories = db.getCategories(projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@LabelBillsActivity, projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
val categories = syncedCategories + hardcoded
db.ensureDefaultLabels(projectId, projectType)
// Reload from DB to get real IDs
val categories = db.getCategories(projectId)
Quadruple(members, billsToLabel, categories, allCategorized)
}
viewModel.billsToLabel = billsToLabel
viewModel.categories = categories
viewModel.categoriesMap = categories.associateBy { it.remoteId }
viewModel.categoriesMap = categories.associateBy { it.id }
viewModel.allCategorizedBills = allCategorized
viewModel.updateSuggestions()
@@ -42,7 +42,7 @@ fun LabelBillsScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.label_bills_title)) },
title = { Text(stringResource(R.string.title_label_bills)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -64,10 +64,11 @@ fun LabelBillsScreen(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.label_bills_suggested),
text = stringResource(R.string.label_bills_suggested).uppercase(),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
@@ -83,7 +84,7 @@ fun LabelBillsScreen(
CategoryButton(
icon = category.icon,
name = category.name ?: "",
onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) }
onClick = { viewModel.labelCurrentBill(db, category.id) }
)
}
}
@@ -93,7 +94,7 @@ fun LabelBillsScreen(
}
} else {
Text(
text = stringResource(R.string.label_bills_no_suggestions),
text = stringResource(R.string.msg_no_suggestions),
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f),
modifier = Modifier.fillMaxWidth(),
@@ -105,10 +106,11 @@ fun LabelBillsScreen(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.setting_category),
text = stringResource(R.string.label_category).uppercase(),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
@@ -124,7 +126,7 @@ fun LabelBillsScreen(
CategoryButton(
icon = category.icon,
name = category.name ?: "",
onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) }
onClick = { viewModel.labelCurrentBill(db, category.id) }
)
}
}
@@ -141,7 +143,7 @@ fun LabelBillsScreen(
Spacer(modifier = Modifier.height(8.dp))
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.label_bills_no_more))
Text(stringResource(R.string.msg_bill_labeled_done))
}
}
}
@@ -243,16 +245,16 @@ fun CategoryButton(icon: String, name: String, onClick: () -> Unit) {
fun LabelBillsScreenPreview() {
val viewModel = LabelBillsViewModel().apply {
billsToLabel = listOf(
DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0, null, -1)
DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0L, null, -1L)
)
val cats = listOf(
DBCategory(1, 1, 1, "Groceries", "🛒", ""),
DBCategory(2, 2, 1, "Leisure", "🥳", ""),
DBCategory(3, 3, 1, "Rent", "🏠", ""),
DBCategory(4, 4, 1, "Bills", "💸", "")
DBCategory(1L, 1L, 1L, "Groceries", "🛒", ""),
DBCategory(2L, 2L, 1L, "Leisure", "🥳", ""),
DBCategory(3L, 3L, 1L, "Rent", "🏠", ""),
DBCategory(4L, 4L, 1L, "Bills", "💸", "")
)
categories = cats
categoriesMap = cats.associateBy { it.remoteId }
categoriesMap = cats.associateBy { it.id }
}
val members = listOf(
DBMember(1L, 0, 1L, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
@@ -43,18 +43,18 @@ class LabelBillsViewModel : ViewModel() {
otherName == name || (name.length > 3 && otherName.contains(name)) || (otherName.length > 3 && name.contains(otherName))
}
val counts = matches.groupBy { it.categoryRemoteId }
val counts = matches.groupBy { it.categoryId }
.mapValues { it.value.size }
.toList()
.sortedByDescending { it.second }
.take(2)
suggestedCategories = counts.mapNotNull { (catId, _) ->
categoriesMap[catId.toLong()]
categoriesMap[catId]
}
}
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Int) {
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) {
currentBill?.let { bill ->
db.updateBillAndSync(
bill = bill,
@@ -65,11 +65,11 @@ class LabelBillsViewModel : ViewModel() {
newOwersIds = bill.billOwersIds,
newRepeat = bill.repeat,
newPaymentMode = bill.paymentMode,
newPaymentModeRemoteId = bill.paymentModeRemoteId,
newPaymentModeId = bill.paymentModeId,
newCategoryId = categoryId,
newComment = bill.comment
)
bill.categoryRemoteId = categoryId
bill.categoryId = categoryId
onBillProcessed?.invoke(bill.id)
moveToNext()
}
@@ -87,11 +87,11 @@ class LabelBillsViewModel : ViewModel() {
val start = currentBillIndex
var next = (start + 1) % billsToLabel.size
while (next != start && billsToLabel[next].categoryRemoteId != 0) {
while (next != start && billsToLabel[next].categoryId != 0L) {
next = (next + 1) % billsToLabel.size
}
currentBillIndex = if (billsToLabel[next].categoryRemoteId == 0) {
currentBillIndex = if (billsToLabel[next].categoryId == 0L) {
next
} else {
billsToLabel.size
@@ -15,6 +15,7 @@ import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.showToast
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.ICallback
@@ -32,9 +33,11 @@ class ManageCurrenciesActivity : AppCompatActivity() {
if (message.isEmpty()) {
showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
} else {
viewModel.showDialog(title=getString(R.string.error_edit_remote_project_helper, message),
message=getString(R.string.currency_manager),
positiveText = getString(android.R.string.ok))
viewModel.showDialog(
title = getString(R.string.dialog_sync_error_title),
message = getString(R.string.error_edit_remote_project_helper, message),
positiveText = getString(android.R.string.ok)
)
}
}
override fun onScheduled() {}
@@ -66,8 +69,10 @@ class ManageCurrenciesActivity : AppCompatActivity() {
viewModel = viewModel,
onBack = { finish() },
onSaveMain = { saveMainCurrency() },
onAdd = { addCurrency() },
onDelete = { deleteCurrency(it) }
onAdd = { addOrUpdateCurrency() },
onDelete = { deleteCurrency(it) },
onEdit = { startEditing(it) },
onCancelEdit = { cancelEditing() }
)
}
}
@@ -86,11 +91,17 @@ class ManageCurrenciesActivity : AppCompatActivity() {
val project = db!!.getProject(selectedProjectID)
if (project != null) {
db!!.syncIfRemote(project)
withContext(Dispatchers.Main) {
if (!db!!.cowspentServerSyncHelper
.editRemoteProject(selectedProjectID, project.name, null, null, newMainCurrencyName, editMainCurrencyCallBack)
) {
showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG)
if (project.type == ProjectType.COSPEND) {
withContext(Dispatchers.Main) {
if (!db!!.cowspentServerSyncHelper
.editRemoteProject(selectedProjectID, project.name, null, null, newMainCurrencyName, editMainCurrencyCallBack)
) {
showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG)
}
}
} else {
withContext(Dispatchers.Main) {
showToast(this@ManageCurrenciesActivity, getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
}
}
}
@@ -98,22 +109,44 @@ class ManageCurrenciesActivity : AppCompatActivity() {
}
}
private fun addCurrency() {
private fun addOrUpdateCurrency() {
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
val newCurrency = DBCurrency(
0, 0, selectedProjectID,
viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
)
val currencyName = viewModel.newCurrencyName
val editingId = viewModel.editingCurrencyId
lifecycleScope.launch {
withContext(Dispatchers.IO) {
db!!.addCurrencyAndSync(newCurrency)
if (editingId != null) {
db!!.updateCurrency(editingId, currencyName, exchangeRate)
val currency = db!!.getCurrency(editingId)
if (currency != null) {
db!!.setCurrencyStateSync(editingId, DBBill.STATE_EDITED)
}
} else {
val newCurrency = DBCurrency(
0, 0, selectedProjectID,
currencyName, exchangeRate, DBBill.STATE_ADDED
)
db!!.addCurrencyAndSync(newCurrency)
}
}
viewModel.newCurrencyName = ""
viewModel.newCurrencyRate = ""
cancelEditing()
updateCurrenciesList()
}
}
private fun startEditing(currency: DBCurrency) {
viewModel.editingCurrencyId = currency.id
viewModel.newCurrencyName = currency.name ?: ""
viewModel.newCurrencyRate = currency.exchangeRate.toString()
}
private fun cancelEditing() {
viewModel.editingCurrencyId = null
viewModel.newCurrencyName = ""
viewModel.newCurrencyRate = ""
}
private fun deleteCurrency(currency: DBCurrency) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
@@ -1,22 +1,57 @@
package net.helcel.cowspent.android.currencies
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.AlertDialog
import net.helcel.cowspent.android.helper.formatAmount
import net.helcel.cowspent.model.DBCurrency
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun ManageCurrenciesScreen(
@@ -24,7 +59,9 @@ fun ManageCurrenciesScreen(
onBack: () -> Unit,
onSaveMain: () -> Unit,
onAdd: () -> Unit,
onDelete: (DBCurrency) -> Unit
onDelete: (DBCurrency) -> Unit,
onEdit: (DBCurrency) -> Unit,
onCancelEdit: () -> Unit
) {
val dialogState = viewModel.dialogState
if (dialogState != null) {
@@ -59,7 +96,7 @@ fun ManageCurrenciesScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.currency_manager)) },
title = { Text(stringResource(R.string.action_currencies)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -73,55 +110,152 @@ fun ManageCurrenciesScreen(
Column(
modifier = Modifier
.padding(padding)
.padding(16.dp)
.imePadding()
.fillMaxSize()
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.02f))
) {
Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6)
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = viewModel.mainCurrencyName,
onValueChange = { viewModel.mainCurrencyName = it },
label = { Text(stringResource(R.string.currency_edit_name)) },
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = onSaveMain, enabled = viewModel.mainCurrencyName.isNotEmpty()) {
Text(stringResource(R.string.save_or_discard_bill_dialog_save))
LaunchedEffect(viewModel.mainCurrencyName) {
if (viewModel.mainCurrencyName.isNotEmpty()) {
delay(500.milliseconds)
onSaveMain()
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = viewModel.newCurrencyName,
onValueChange = { viewModel.newCurrencyName = it },
label = { Text(stringResource(R.string.currency_edit_name)) },
modifier = Modifier.weight(1f)
Column(modifier = Modifier.padding(16.dp)) {
// Section 1: Main Currency
Text(
text = stringResource(R.string.main_currency).uppercase(),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
Spacer(modifier = Modifier.width(8.dp))
OutlinedTextField(
value = viewModel.newCurrencyRate,
onValueChange = { viewModel.newCurrencyRate = it },
label = { Text(stringResource(R.string.currency_rate)) },
modifier = Modifier.weight(1f)
Card(
shape = RoundedCornerShape(12.dp),
elevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = viewModel.mainCurrencyName,
onValueChange = { viewModel.mainCurrencyName = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("e.g. EUR", fontSize = 14.sp) },
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent
)
)
}
Spacer(modifier = Modifier.height(24.dp))
// Section 2: Exchange Rates
val isEditing = viewModel.editingCurrencyId != null
Text(
text = (if (isEditing) "Edit exchange rate" else "Add exchange rate").uppercase(),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
// Integrated Add/Edit Ribbon
Surface(
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth(),
elevation = 2.dp
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("1 ", style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontWeight = FontWeight.Bold)
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 4.dp)
)
OutlinedTextField(
value = viewModel.newCurrencyRate,
onValueChange = { viewModel.newCurrencyRate = it },
modifier = Modifier.weight(1.2f).height(52.dp),
placeholder = { Text("1", fontSize = 12.sp) },
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.ExtraBold, fontSize = 14.sp),
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.02f)
)
)
Spacer(modifier = Modifier.width(8.dp))
OutlinedTextField(
value = viewModel.newCurrencyName,
onValueChange = { viewModel.newCurrencyName = it },
modifier = Modifier.weight(1f).height(52.dp),
placeholder = { Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontSize = 12.sp) },
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold, fontSize = 14.sp),
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.02f)
)
)
Row(modifier = Modifier.padding(start = 4.dp)) {
IconButton(
onClick = onAdd,
enabled = viewModel.isAddEnabled()
) {
Icon(
imageVector = if (isEditing) Icons.Default.Done else Icons.Default.Add,
contentDescription = null,
tint = if (viewModel.isAddEnabled()) {
if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
} else MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
)
}
if (isEditing) {
IconButton(onClick = onCancelEdit) {
Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.error)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "SAVED RATES",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onAdd,
modifier = Modifier.fillMaxWidth(),
enabled = viewModel.isAddEnabled()
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(stringResource(R.string.simple_add))
}
Spacer(modifier = Modifier.height(24.dp))
LazyColumn(modifier = Modifier.weight(1f)) {
items(viewModel.currencies) { currency ->
CurrencyRow(currency, onDelete = { onDelete(currency) })
CurrencyRow(
currency = currency,
mainCurrencyName = viewModel.mainCurrencyName,
isEditing = viewModel.editingCurrencyId == currency.id,
onEdit = { onEdit(currency) },
onDelete = { onDelete(currency) }
)
}
}
}
@@ -129,20 +263,70 @@ fun ManageCurrenciesScreen(
}
@Composable
fun CurrencyRow(currency: DBCurrency, onDelete: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
fun CurrencyRow(
currency: DBCurrency,
mainCurrencyName: String,
isEditing: Boolean,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
Card(
shape = RoundedCornerShape(12.dp),
elevation = if (isEditing) 4.dp else 1.dp,
border = if (isEditing) BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.5f)) else null,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.weight(1f)) {
Text(currency.name ?: "", style = MaterialTheme.typography.subtitle1)
Text("Rate: ${currency.exchangeRate}", style = MaterialTheme.typography.caption)
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onEdit)
.padding(16.dp, 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "1 ",
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Text(
text = mainCurrencyName.ifEmpty { "$" },
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = " = ",
style = MaterialTheme.typography.h6,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 8.dp)
)
Text(
text = formatAmount(currency.exchangeRate),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.ExtraBold,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
)
Text(
text = " ${currency.name ?: ""}",
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colors.error.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp)
)
}
}
}
Divider()
}
@Preview(showBackground = true)
@@ -151,6 +335,9 @@ fun CurrencyRowPreview() {
MaterialTheme {
CurrencyRow(
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
mainCurrencyName = "EUR",
isEditing = false,
onEdit = {},
onDelete = {}
)
}
@@ -172,7 +359,9 @@ fun ManageCurrenciesScreenPreview() {
onBack = {},
onSaveMain = {},
onAdd = {},
onDelete = {}
onDelete = {},
onEdit = {},
onCancelEdit = {}
)
}
}
@@ -13,6 +13,8 @@ class ManageCurrenciesViewModel : ViewModel() {
var newCurrencyName by mutableStateOf("")
var newCurrencyRate by mutableStateOf("")
var editingCurrencyId by mutableStateOf<Long?>(null)
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
var dialogState by mutableStateOf<DialogState?>(null)
@@ -11,6 +11,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
@@ -23,9 +24,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.android.helper.formatBalance
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
import net.helcel.cowspent.android.helper.TextIcon
import net.helcel.cowspent.android.helper.TextIconDisplay
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
@@ -38,6 +41,7 @@ fun Drawer(
selectedProjectId: Long,
selectedMemberId: Long?,
lastSyncText: String,
mainCurrency: String? = null,
showArchived: Boolean = false,
onProjectClick: (Long) -> Unit,
onProjectOptionsClick: (Long) -> Unit,
@@ -96,22 +100,40 @@ fun Drawer(
// Members Section
val membersState = rememberLazyListState()
val sortedMembers = remember(members, memberBalances, selectedMemberId) {
members.filter {
val balance = memberBalances[it.id] ?: 0.0
it.isActivated || balance > 0.01 || balance < -0.01 || it.id == selectedMemberId
}.sortedWith(compareBy<DBMember> {
val balance = memberBalances[it.id] ?: 0.0
when {
balance > 0.01 -> 0
balance < -0.01 -> 1
else -> 2
}
}.thenBy {
val balance = memberBalances[it.id] ?: 0.0
if (balance > 0.01) -balance else balance
}.thenBy { it.name })
}
LazyColumn(
state = membersState,
modifier = Modifier
.heightIn(max = maxBottomHeight)
.lazyVerticalScrollbar(membersState)
) {
if (members.isNotEmpty()) {
if (sortedMembers.isNotEmpty()) {
item {
DrawerItem(
icon = Icons.Default.Receipt,
text = stringResource(R.string.label_all_bills),
secondaryText = mainCurrency,
selected = selectedMemberId == null,
onClick = { onMemberClick(null) }
)
}
items(members) { member ->
items(sortedMembers) { member ->
val balance = memberBalances[member.id] ?: 0.0
MemberDrawerItem(
member = member,
@@ -138,20 +160,27 @@ private fun DrawerHeader(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary)
.padding(16.dp, 4.dp)
.padding(4.dp, 4.dp)
) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Box(modifier = Modifier.size(48.dp)) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
stringResource(id = R.string.app_name),
text = stringResource(id = R.string.app_name),
color = MaterialTheme.colors.onPrimary,
fontSize = 18.sp,
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.weight(1f))
@@ -178,6 +207,7 @@ private fun DrawerHeader(
}
if (lastSyncText.isNotEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.width(16.dp))
Icon(
Icons.Default.Sync,
contentDescription = null,
@@ -241,6 +271,7 @@ fun DrawerItem(
member: DBMember? = null,
text: String? = null,
balanceText: String? = null,
secondaryText: String? = null,
balanceColor: Color = Color.Unspecified,
selected: Boolean = false,
alpha: Float = 1f,
@@ -259,13 +290,8 @@ fun DrawerItem(
verticalAlignment = Alignment.CenterVertically
) {
if (member != null) {
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
avatar = member.avatar,
disabled = !member.isActivated,
MemberAvatar(
member = member,
size = 24.dp,
alpha = alpha
)
@@ -273,7 +299,7 @@ fun DrawerItem(
Icon(icon, contentDescription = null, tint = contentColor.copy(alpha = 0.6f * alpha))
}
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(32.dp))
val itemText = text ?: member?.name ?: ""
Text(
@@ -295,6 +321,17 @@ fun DrawerItem(
)
}
if (secondaryText != null) {
Box(modifier = Modifier
.padding(end = 8.dp)
) {
TextIconDisplay(
textIcon = TextIcon.Symbol(secondaryText),
tint = contentColor.copy(alpha = 0.6f)
)
}
}
if (onSecondaryClick != null) {
IconButton(onClick = onSecondaryClick) {
Icon(
@@ -340,6 +377,7 @@ fun DrawerPreview() {
selectedProjectId = 1,
selectedMemberId = null,
lastSyncText = "Last sync: 5 mins ago",
mainCurrency = "EUR",
onProjectClick = {},
onProjectOptionsClick = {},
onMemberClick = {},
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -110,28 +111,55 @@ fun AlertDialog(
if (message != null) Spacer(Modifier.height(8.dp))
LazyColumn {
itemsIndexed(items) { index, item ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
}
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
val text = item.toString()
if (text.contains("|")) {
val parts = text.split("|")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = parts[0].trim(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = parts[1].trim(),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
} else {
Text(
text = text,
style = MaterialTheme.typography.body1
)
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
Text(
text = item.toString(),
style = MaterialTheme.typography.body1
)
if (index < items.size - 1) {
Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
}
}
}
}
@@ -202,7 +230,12 @@ fun AlertDialogContent(
Spacer(Modifier.width(8.dp))
}
if (title != null) {
Text(text = title, style = MaterialTheme.typography.h6)
Text(
text = title.uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
}
}
}
@@ -216,28 +249,55 @@ fun AlertDialogContent(
if (message != null) Spacer(Modifier.height(8.dp))
LazyColumn {
itemsIndexed(items) { index, item ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemSelected?.invoke(index)
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
val text = item.toString()
if (text.contains("|")) {
val parts = text.split("|")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = parts[0].trim(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
Text(
text = parts[1].trim(),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
} else {
Text(
text = text,
style = MaterialTheme.typography.body1
)
}
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (itemIcons != null && index < itemIcons.size) {
Icon(
itemIcons[index],
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(Modifier.width(16.dp))
}
Text(
text = item.toString(),
style = MaterialTheme.typography.body1
)
if (index < items.size - 1) {
Divider(color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
}
}
}
}
@@ -188,12 +188,13 @@ fun ColorPicker(
Spacer(modifier = Modifier.height(16.dp))
val hueBrush = remember {
Brush.horizontalGradient(
listOf(
Color.Red, Color.Yellow, Color.Green, Color.Cyan, Color.Blue, Color.Magenta, Color.Red
)
)
val hueBrush = remember(lightness, chroma) {
val hueSteps = 36 // More steps for smoother gradient
val colors = (0..hueSteps).map { step ->
val h = (step.toFloat() / hueSteps) * 360f
Color(mLCHtoRBG(lightness, chroma, h))
}
Brush.horizontalGradient(colors)
}
LchSlider(
label = "H",
@@ -1,20 +1,80 @@
package net.helcel.cowspent.android.helper
import net.helcel.cowspent.util.SupportUtil
import java.text.NumberFormat
import java.util.Locale
import kotlin.math.abs
import kotlin.math.round
/**
* Formats amount using k/M notation (e.g., 1.5k for 1500).
*/
fun formatShortValue(value: Double): String {
val absValue = abs(value)
val sign = if (value < 0) "-" else ""
return when {
value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "")
value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "")
else -> String.format(Locale.ROOT, "%.0f", value)
absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M"
absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k"
else -> sign + formatAmount(absValue)
}
}
/**
* Formats balance with sign and locale-aware number formatting.
*/
fun formatBalance(balance: Double): String {
val rbalance = round(abs(balance) * 100.0) / 100.0
val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else ""
return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(rbalance)}"
return if (rbalance == 0.0) "" else "$balanceSign${formatAmount(rbalance)}"
}
/**
* Formats amount using system locale or specified locale.
*/
fun formatAmount(value: Double, locale: Locale = Locale.getDefault()): String {
val formatter = NumberFormat.getNumberInstance(locale)
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 0
return formatter.format(value)
}
/**
* Parses amount from string robustly.
* Tries common separators and cleans up non-numeric characters (except separators).
*/
fun parseAmount(input: String?): Double? {
if (input.isNullOrBlank()) return null
// Remove non-numeric characters except for delimiters and minus sign
val cleaned = input.replace(Regex("[^0-9,.-]"), "").trim()
if (cleaned.isEmpty()) return null
// If there's both a comma and a dot, we assume the last one is the decimal separator
val lastDot = cleaned.lastIndexOf('.')
val lastComma = cleaned.lastIndexOf(',')
return if (lastDot != -1 && lastComma != -1) {
if (lastDot > lastComma) {
// Dot is decimal, comma is thousands
cleaned.replace(",", "").toDoubleOrNull()
} else {
// Comma is decimal, dot is thousands
cleaned.replace(".", "").replace(",", ".").toDoubleOrNull()
}
} else if (lastComma != -1) {
// Only comma. Could be decimal or thousands.
if (cleaned.count { it == ',' } == 1) {
cleaned.replace(',', '.').toDoubleOrNull()
} else {
// Multiple commas -> thousands
cleaned.replace(",", "").toDoubleOrNull()
}
} else {
// Only dots or no separators
if (cleaned.count { it == '.' } > 1) {
cleaned.replace(".", "").toDoubleOrNull()
} else {
cleaned.toDoubleOrNull()
}
}
}
@@ -0,0 +1,33 @@
package net.helcel.cowspent.android.helper
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
sealed class TextIcon {
data class Symbol(val text: String) : TextIcon()
}
/**
* A helper Composable to display a [TextIcon].
*/
@Composable
fun TextIconDisplay(
textIcon: TextIcon,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current
) {
when (textIcon) {
is TextIcon.Symbol -> Text(
text = textIcon.text,
modifier = modifier,
color = tint,
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold
)
}
}
@@ -4,37 +4,49 @@ import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import net.helcel.cowspent.theme.ThemeUtils
import androidx.camera.core.*
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.zxing.*
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
import net.helcel.cowspent.R
import net.helcel.cowspent.android.main.MainConstants
import net.helcel.cowspent.theme.ThemeUtils
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -122,7 +134,7 @@ fun QrCodeScannerScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.scan_qrcode)) },
title = { Text(stringResource(R.string.action_scan_qrcode)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import android.graphics.BitmapFactory
import android.util.Base64
import net.helcel.cowspent.model.DBMember
@Composable
fun UserAvatar(
@@ -91,6 +92,26 @@ fun UserAvatar(
}
}
@Composable
fun MemberAvatar(
member: DBMember,
modifier: Modifier = Modifier,
size: Dp = 40.dp,
alpha: Float = 1f
) {
UserAvatar(
name = member.name,
modifier = modifier,
size = size,
r = member.r,
g = member.g,
b = member.b,
avatar = member.avatar,
disabled = !member.isActivated,
alpha = alpha
)
}
@Preview
@Composable
fun UserAvatarPreview() {
@@ -0,0 +1,46 @@
package net.helcel.cowspent.android.label
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import net.helcel.cowspent.theme.ThemeUtils
class LabelManagementActivity : AppCompatActivity() {
private val viewModel: LabelManagementViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, 0L)
if (projectId == 0L) {
finish()
return
}
viewModel.loadLabels(projectId)
setContent {
ThemeUtils.CowspentTheme {
LabelManagementScreen(
viewModel = viewModel,
onBack = { finish() }
)
}
}
}
companion object {
const val EXTRA_PROJECT_ID = "EXTRA_PROJECT_ID"
fun createIntent(context: Context, projectId: Long): Intent {
return Intent(context, LabelManagementActivity::class.java).apply {
putExtra(EXTRA_PROJECT_ID, projectId)
}
}
}
}
@@ -0,0 +1,399 @@
package net.helcel.cowspent.android.label
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.ColorPicker
import net.helcel.cowspent.android.helper.StatefulAlertDialog
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode
@Composable
fun LabelManagementScreen(
viewModel: LabelManagementViewModel,
onBack: () -> Unit
) {
LabelManagementScreenContent(
categories = viewModel.categories,
paymentModes = viewModel.paymentModes,
dialogState = viewModel.dialogState,
onBack = onBack,
onAddCategory = viewModel::addCategory,
onUpdateCategory = viewModel::updateCategory,
onDeleteCategory = viewModel::deleteCategory,
onAddPaymentMode = viewModel::addPaymentMode,
onUpdatePaymentMode = viewModel::updatePaymentMode,
onDeletePaymentMode = viewModel::deletePaymentMode,
onDismissDialog = { viewModel.dialogState = null },
onShowDialog = { viewModel.dialogState = it }
)
}
@Composable
fun LabelManagementScreenContent(
categories: List<DBCategory>,
paymentModes: List<DBPaymentMode>,
dialogState: net.helcel.cowspent.android.helper.DialogState?,
onBack: () -> Unit,
onAddCategory: (String, String, String) -> Unit,
onUpdateCategory: (DBCategory, String, String, String) -> Unit,
onDeleteCategory: (Long) -> Unit,
onAddPaymentMode: (String, String, String) -> Unit,
onUpdatePaymentMode: (DBPaymentMode, String, String, String) -> Unit,
onDeletePaymentMode: (Long) -> Unit,
onDismissDialog: () -> Unit,
onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit,
initialTab: Int = 0
) {
var selectedTab by remember { mutableIntStateOf(initialTab) }
val tabs = listOf(
stringResource(R.string.label_categories),
stringResource(R.string.label_payment_modes)
)
val deleteLabelTitle = stringResource(R.string.delete_label_confirmation_title)
val deleteLabelMessage = stringResource(R.string.delete_label_confirmation_message)
val yesText = stringResource(R.string.simple_yes)
val noText = stringResource(R.string.simple_no)
var showEditDialog by remember { mutableStateOf(false) }
var editingCategory by remember { mutableStateOf<DBCategory?>(null) }
var editingPaymentMode by remember { mutableStateOf<DBPaymentMode?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.title_labels)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
backgroundColor = MaterialTheme.colors.primary,
elevation = 0.dp
)
},
floatingActionButton = {
FloatingActionButton(onClick = {
if (selectedTab == 0) {
editingCategory = null
showEditDialog = true
} else {
editingPaymentMode = null
showEditDialog = true
}
}) {
Icon(Icons.Default.Add, contentDescription = null)
}
}
) { padding ->
Column(modifier = Modifier.padding(padding)) {
TabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) }
)
}
}
if (selectedTab == 0) {
CategoryList(
categories = categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT },
onEdit = {
editingCategory = it
showEditDialog = true
},
onDelete = { category ->
onShowDialog(net.helcel.cowspent.android.helper.DialogState(
title = deleteLabelTitle,
message = deleteLabelMessage,
positiveText = yesText,
negativeText = noText,
onConfirm = { onDeleteCategory(category.id) }
))
}
)
} else {
PaymentModeList(
paymentModes = paymentModes,
onEdit = {
editingPaymentMode = it
showEditDialog = true
},
onDelete = { pm ->
onShowDialog(net.helcel.cowspent.android.helper.DialogState(
title = deleteLabelTitle,
message = deleteLabelMessage,
positiveText = yesText,
negativeText = noText,
onConfirm = { onDeletePaymentMode(pm.id) }
))
}
)
}
}
}
if (showEditDialog) {
if (selectedTab == 0) {
EditLabelDialog(
title = if (editingCategory == null) stringResource(R.string.title_add_category) else stringResource(R.string.action_edit),
initialName = editingCategory?.name ?: "",
initialIcon = editingCategory?.icon ?: "",
initialColor = editingCategory?.color ?: "#FF0000",
onDismiss = { showEditDialog = false },
onSave = { name, icon, color ->
if (editingCategory == null) {
onAddCategory(name, icon, color)
} else {
onUpdateCategory(editingCategory!!, name, icon, color)
}
showEditDialog = false
}
)
} else {
EditLabelDialog(
title = if (editingPaymentMode == null) stringResource(R.string.title_add_payment_mode) else stringResource(R.string.action_edit),
initialName = editingPaymentMode?.name ?: "",
initialIcon = editingPaymentMode?.icon ?: "",
initialColor = editingPaymentMode?.color ?: "#00FF00",
onDismiss = { showEditDialog = false },
onSave = { name, icon, color ->
if (editingPaymentMode == null) {
onAddPaymentMode(name, icon, color)
} else {
onUpdatePaymentMode(editingPaymentMode!!, name, icon, color)
}
showEditDialog = false
}
)
}
}
StatefulAlertDialog(
state = dialogState,
onDismissRequest = onDismissDialog
)
}
@Composable
fun CategoryList(
categories: List<DBCategory>,
onEdit: (DBCategory) -> Unit,
onDelete: (DBCategory) -> Unit
) {
LazyColumn {
items(categories) { category ->
LabelItem(
name = category.name ?: "",
icon = category.icon,
color = category.color,
onEdit = { onEdit(category) },
onDelete = { onDelete(category) }
)
}
}
}
@Composable
fun PaymentModeList(
paymentModes: List<DBPaymentMode>,
onEdit: (DBPaymentMode) -> Unit,
onDelete: (DBPaymentMode) -> Unit
) {
LazyColumn {
items(paymentModes) { pm ->
LabelItem(
name = pm.name ?: "",
icon = pm.icon,
color = pm.color,
onEdit = { onEdit(pm) },
onDelete = { onDelete(pm) }
)
}
}
}
@Composable
fun LabelItem(
name: String,
icon: String,
color: String,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onEdit() }
.padding(16.dp, 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
.background(parseColor(color)),
contentAlignment = Alignment.Center
) {
Text(text = icon, fontSize = 12.sp)
}
Spacer(modifier = Modifier.width(32.dp))
Text(text = name, modifier = Modifier.weight(1f), style = MaterialTheme.typography.subtitle1)
IconButton(onClick = onEdit) {
Icon(Icons.Default.Edit, contentDescription = null, tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error.copy(alpha = 0.6f))
}
}
}
@Composable
fun EditLabelDialog(
title: String,
initialName: String,
initialIcon: String,
initialColor: String,
onDismiss: () -> Unit,
onSave: (String, String, String) -> Unit
) {
var name by remember { mutableStateOf(initialName) }
var icon by remember { mutableStateOf(initialIcon) }
var color by remember { mutableStateOf(initialColor) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
OutlinedTextField(
value = name,
onValueChange = { name = it },
placeholder = { Text(stringResource(R.string.label_name)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = icon,
onValueChange = { if (it.length <= 2) icon = it },
placeholder = { Text(stringResource(R.string.label_icon)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.caption)
ColorPicker(
initialColor = parseColorInt(color),
onColorChanged = { color = String.format("#%06X", 0xFFFFFF and it) }
)
}
},
confirmButton = {
Button(
onClick = { onSave(name, icon, color) },
enabled = name.isNotBlank()
) {
Text(stringResource(R.string.action_save))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.simple_cancel))
}
}
)
}
fun parseColor(colorString: String): Color {
return try {
Color(colorString.toColorInt())
} catch (_: Exception) {
Color.Gray
}
}
fun parseColorInt(colorString: String): Int {
return try {
colorString.toColorInt()
} catch (_: Exception) {
Color.Gray.toArgb()
}
}
@Preview(showBackground = true)
@Composable
fun LabelManagementCategoriesPreview() {
MaterialTheme {
LabelManagementScreenContent(
categories = listOf(
DBCategory(1L, 1L, 1L, "Groceries", "🛒", "#FF0000"),
DBCategory(2L, 2L, 1L, "Rent", "🏠", "#00FF00")
),
paymentModes = emptyList(),
dialogState = null,
onBack = {},
onAddCategory = { _, _, _ -> },
onUpdateCategory = { _, _, _, _ -> },
onDeleteCategory = { _ -> },
onAddPaymentMode = { _, _, _ -> },
onUpdatePaymentMode = { _, _, _, _ -> },
onDeletePaymentMode = { _ -> },
onDismissDialog = {},
onShowDialog = {},
initialTab = 0
)
}
}
@Preview(showBackground = true)
@Composable
fun LabelManagementPaymentModesPreview() {
MaterialTheme {
LabelManagementScreenContent(
categories = emptyList(),
paymentModes = listOf(
DBPaymentMode(1L, 1L, 1L, "Cash", "💵", "#0000FF"),
DBPaymentMode(2L, 2L, 1L, "Credit Card", "💳", "#FFFF00")
),
dialogState = null,
onBack = {},
onAddCategory = { _, _, _ -> },
onUpdateCategory = { _, _, _, _ -> },
onDeleteCategory = { _ -> },
onAddPaymentMode = { _, _, _ -> },
onUpdatePaymentMode = { _, _, _, _ -> },
onDeletePaymentMode = { _ -> },
onDismissDialog = {},
onShowDialog = {},
initialTab = 1
)
}
}
@@ -0,0 +1,95 @@
package net.helcel.cowspent.android.label
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
class LabelManagementViewModel(application: Application) : AndroidViewModel(application) {
private val db = CowspentSQLiteOpenHelper.getInstance(application)
var projectId by mutableLongStateOf(0L)
var categories by mutableStateOf<List<DBCategory>>(emptyList())
var paymentModes by mutableStateOf<List<DBPaymentMode>>(emptyList())
var dialogState by mutableStateOf<DialogState?>(null)
fun loadLabels(projId: Long) {
projectId = projId
viewModelScope.launch {
val project = withContext(Dispatchers.IO) { db.getProject(projId) }
if (project != null) {
withContext(Dispatchers.IO) {
db.ensureDefaultLabels(projId, project.type)
}
}
categories = withContext(Dispatchers.IO) { db.getCategories(projId) }
paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
}
}
fun addCategory(name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.addCategoryAndSync(DBCategory(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED))
}
loadLabels(projectId)
}
}
fun updateCategory(cat: DBCategory, name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.updateCategoryAndSync(cat, name, icon, color)
}
loadLabels(projectId)
}
}
fun deleteCategory(id: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.deleteCategoryAndSync(id)
}
loadLabels(projectId)
}
}
fun addPaymentMode(name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.addPaymentModeAndSync(DBPaymentMode(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED))
}
loadLabels(projectId)
}
}
fun updatePaymentMode(pm: DBPaymentMode, name: String, icon: String, color: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.updatePaymentModeAndSync(pm, name, icon, color)
}
loadLabels(projectId)
}
}
fun deletePaymentMode(id: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.deletePaymentModeAndSync(id)
}
loadLabels(projectId)
}
}
}
@@ -2,9 +2,25 @@ package net.helcel.cowspent.android.main
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Receipt
import androidx.compose.material.icons.filled.Repeat
@@ -13,10 +29,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil
@@ -28,15 +45,25 @@ fun EmptyProjectsState(onConfigureNextcloud: () -> Unit, onAddManually: () -> Un
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_projects_title), style = MaterialTheme.typography.h6)
Text(
text = stringResource(R.string.error_no_projects).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(stringResource(R.string.no_projects_text))
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onConfigureNextcloud) {
Text(
text = stringResource(R.string.no_projects_text),
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onConfigureNextcloud, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(R.string.configure_account_choice))
}
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = onAddManually) {
Button(onClick = onAddManually, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(R.string.add_project_choice))
}
}
@@ -49,9 +76,19 @@ fun EmptyMembersState() {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_members_title), style = MaterialTheme.typography.h6)
Text(
text = stringResource(R.string.error_no_members).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(stringResource(R.string.no_members_text))
Text(
text = stringResource(R.string.no_members_text),
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
@@ -62,9 +99,19 @@ fun EmptyBillsState() {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_bills_title), style = MaterialTheme.typography.h6)
Text(
text = stringResource(R.string.error_no_bills).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(stringResource(R.string.no_bills_text))
Text(
text = stringResource(R.string.no_bills_text),
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
@@ -83,13 +130,8 @@ fun BillItemRow(bill: DBBill, payer: DBMember?, onClick: () -> Unit) {
) {
if (payer != null) {
Box {
UserAvatar(
name = payer.name,
r = payer.r,
g = payer.g,
b = payer.b,
avatar = payer.avatar,
disabled = !payer.isActivated,
MemberAvatar(
member = payer,
size = 40.dp
)
if (bill.repeat != null && bill.repeat != DBBill.NON_REPEATED) {
@@ -111,15 +153,21 @@ fun BillItemRow(bill: DBBill, payer: DBMember?, onClick: () -> Unit) {
Icons.Default.Receipt,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(bill.formattedWhat.ifEmpty { bill.what }, fontWeight = FontWeight.Bold)
Text(
text = bill.formattedWhat.ifEmpty { bill.what },
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
Text(
text = bill.formattedSubtitle.ifEmpty { bill.comment ?: "" },
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -140,13 +188,16 @@ fun SectionHeader(title: String) {
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxWidth()
) {
Divider(thickness = 2.dp)
Text(
text = title,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.caption.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colors.primary
)
Column {
Divider(thickness = 1.dp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
Text(
text = title.uppercase(),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
}
}
}
@@ -157,7 +208,18 @@ fun EmptyState() {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.no_bills_title), style = MaterialTheme.typography.h6)
Text(stringResource(R.string.no_bills_text), modifier = Modifier.padding(16.dp))
Text(
text = stringResource(R.string.error_no_bills).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface
)
Text(
text = stringResource(R.string.no_bills_text),
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
@@ -2,7 +2,6 @@ package net.helcel.cowspent.android.main
import android.annotation.SuppressLint
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -46,6 +45,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -57,14 +57,12 @@ import net.helcel.cowspent.android.drawer.Drawer
import net.helcel.cowspent.android.helper.StatefulAlertDialog
import net.helcel.cowspent.android.project.ProjectOptionsDialogContent
import net.helcel.cowspent.android.project.ProjectShareDialogContent
import net.helcel.cowspent.android.project.member.MemberAddDialogContent
import net.helcel.cowspent.android.project.member.MemberEditDialogContent
import net.helcel.cowspent.android.project.member.MemberManagementDialogContent
import net.helcel.cowspent.android.project.settle.ProjectSettlementDialogContent
import net.helcel.cowspent.android.statistics.ProjectStatisticsActivity
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.model.SectionItem
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.IRefreshBillsListCallback
@@ -103,9 +101,12 @@ fun BillsListScreen(
val pullRefreshState = rememberPullRefreshState(viewModel.isRefreshing, onRefresh)
val context = LocalContext.current
val memberAlreadyExistsError = stringResource(R.string.member_already_exists)
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
var showBetaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
StatefulAlertDialog(
state = viewModel.dialogState,
@@ -137,6 +138,10 @@ fun BillsListScreen(
onProjectAction(projectOptionsProjectId, 3)
viewModel.showProjectOptionsDialogByProjectId = null
},
onManageLabels = {
onProjectAction(projectOptionsProjectId, 8)
viewModel.showProjectOptionsDialogByProjectId = null
},
onStatistics = {
onProjectAction(projectOptionsProjectId, 4)
viewModel.showProjectOptionsDialogByProjectId = null
@@ -155,8 +160,10 @@ fun BillsListScreen(
},
onDismiss = { viewModel.showProjectOptionsDialogByProjectId = null },
isArchived = proj?.isArchived == true,
projectType = proj?.type ?: ProjectType.LOCAL,
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
isShareable = proj?.isShareable() ?: true
isShareable = proj?.isShareable() ?: true,
showBetaFeatures = showBetaFeatures
)
}
}
@@ -208,114 +215,17 @@ fun BillsListScreen(
val manageMembersProjectId = viewModel.showMemberManagementDialogByProjectId
if (manageMembersProjectId != null) {
val members = viewModel.members // Use members from ViewModel as they are already loaded
Dialog(
onDismissRequest = { viewModel.showMemberManagementDialogByProjectId = null },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
MemberManagementDialogContent(
members = members,
onAddMember = {
viewModel.showAddMemberDialogByProjectId = manageMembersProjectId
viewModel.showMemberManagementDialogByProjectId = null
},
onEditMember = { member ->
viewModel.showEditMemberDialogByProjectId = member.id
viewModel.showMemberManagementDialogByProjectId = null
},
onDismiss = { viewModel.showMemberManagementDialogByProjectId = null }
)
}
viewModel.showMemberManagementDialogByProjectId = null
}
val addMemberProjectId = viewModel.showAddMemberDialogByProjectId
if (addMemberProjectId != null) {
Dialog(
onDismissRequest = { viewModel.showAddMemberDialogByProjectId = null },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
MemberAddDialogContent(
onAdd = { memberName ->
val memberNames =
db.getMembersOfProject(addMemberProjectId, null).map { it.name }
if (memberNames.contains(memberName)) {
Toast.makeText(
context,
memberAlreadyExistsError,
Toast.LENGTH_SHORT
).show()
} else {
val color = net.helcel.cowspent.android.helper.TextDrawable.getColorFromName(memberName)
db.addMemberAndSync(
DBMember(
0,
0,
addMemberProjectId,
memberName,
true,
1.0,
DBBill.STATE_ADDED,
android.graphics.Color.red(color),
android.graphics.Color.green(color),
android.graphics.Color.blue(color),
null,
null
)
)
refreshCallback.refreshLists(false)
viewModel.showAddMemberDialogByProjectId = null
viewModel.showMemberManagementDialogByProjectId = addMemberProjectId
}
},
onDismiss = {
viewModel.showAddMemberDialogByProjectId = null
viewModel.showMemberManagementDialogByProjectId = addMemberProjectId
}
)
}
viewModel.showAddMemberDialogByProjectId = null
}
val editMemberId = viewModel.showEditMemberDialogByProjectId
if (editMemberId != null) {
val memberToEdit = remember(editMemberId, viewModel.members) {
viewModel.members.find { it.id == editMemberId }
}
if (memberToEdit != null) {
Dialog(
onDismissRequest = { viewModel.showEditMemberDialogByProjectId = null },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
MemberEditDialogContent(
member = memberToEdit,
onSave = { name, weight, isActivated, r, g, b ->
db.updateMemberAndSync(
memberToEdit,
name,
weight,
isActivated,
r,
g,
b,
"",
""
)
refreshCallback.refreshLists(false)
viewModel.showEditMemberDialogByProjectId = null
viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId
},
onDelete = {
db.deleteMember(editMemberId)
refreshCallback.refreshLists(false)
viewModel.showEditMemberDialogByProjectId = null
viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId
},
onDismiss = {
viewModel.showEditMemberDialogByProjectId = null
viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId
}
)
}
}
viewModel.showEditMemberDialogByProjectId = null
}
val shareProjectId = viewModel.showShareDialogByProjectId
@@ -324,8 +234,8 @@ fun BillsListScreen(
viewModel.projects.find { it.id == shareProjectId }
}
if (proj != null) {
val shareIntentTitle = stringResource(R.string.share_share_intent_title, proj.name)
val shareChooserTitle = stringResource(R.string.share_share_chooser_title, proj.name)
val shareIntentTitle = stringResource(R.string.share_intent_title, proj.name)
val shareChooserTitle = stringResource(R.string.share_chooser_title, proj.name)
Dialog(
onDismissRequest = { viewModel.showShareDialogByProjectId = null },
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -358,7 +268,13 @@ fun BillsListScreen(
TextField(
value = viewModel.searchQuery,
onValueChange = { viewModel.searchQuery = it },
placeholder = { Text(stringResource(R.string.action_search), color = MaterialTheme.colors.onPrimary.copy(alpha = 0.7f)) },
placeholder = {
Text(
text = stringResource(R.string.action_search),
color = MaterialTheme.colors.onPrimary.copy(alpha = 0.7f),
style = MaterialTheme.typography.subtitle1
)
},
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
@@ -381,8 +297,20 @@ fun BillsListScreen(
)
} else {
Column {
if (viewModel.title.isNotEmpty()) Text(viewModel.title)
else Text(stringResource(R.string.app_name))
if (viewModel.title.isNotEmpty()) {
Text(
text = viewModel.title,
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold
)
}
else {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold
)
}
}
}
},
@@ -404,7 +332,7 @@ fun BillsListScreen(
},
actions = {
if (!isSearchExpanded) {
if (viewModel.hasUnlabeledBills) {
if (showBetaFeatures && viewModel.hasUnlabeledBills) {
IconButton(onClick = onLabelBillsClick) {
Icon(
Icons.Default.Category,
@@ -425,11 +353,12 @@ fun BillsListScreen(
val selectedProject = viewModel.projects.find { it.id == viewModel.selectedProjectId }
if (selectedProject != null && !selectedProject.isArchived) {
FloatingActionButton(onClick = onAddBillClick) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_create_bill))
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_new_bill))
}
}
},
drawerContent = {
val selectedProject = viewModel.projects.find { it.id == viewModel.selectedProjectId }
Drawer(
projects = viewModel.projects,
members = viewModel.members,
@@ -437,6 +366,7 @@ fun BillsListScreen(
selectedProjectId = viewModel.selectedProjectId,
selectedMemberId = viewModel.selectedMemberId,
lastSyncText = viewModel.lastSyncText,
mainCurrency = selectedProject?.currencyName,
showArchived = showArchived,
onProjectClick = {
viewModel.selectedMemberId = null
@@ -5,6 +5,7 @@ import android.content.Intent
import net.helcel.cowspent.R
import net.helcel.cowspent.model.*
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.IRefreshBillsListCallback
import java.text.SimpleDateFormat
import java.util.*
@@ -65,11 +66,11 @@ object BillsListUtils {
memberIdToName: Map<Long, String>
) {
val projectName = proj.name.ifEmpty { proj.remoteId }
var text = context.getString(R.string.share_settle_intro, projectName) + "\n"
var text = context.getString(R.string.msg_settle_intro, projectName) + "\n"
for (t in transactions) {
val amount = round(t.amount * 100.0) / 100.0
text += "\n" + context.getString(
R.string.share_settle_sentence,
R.string.msg_settle_sentence,
memberIdToName[t.owerMemberId],
memberIdToName[t.receiverMemberId],
amount
@@ -80,12 +81,12 @@ object BillsListUtils {
shareIntent.type = "text/plain"
shareIntent.putExtra(
Intent.EXTRA_SUBJECT,
context.getString(R.string.share_settle_title, projectName)
context.getString(R.string.title_settle)
)
shareIntent.putExtra(Intent.EXTRA_TEXT, text)
val chooserIntent = Intent.createChooser(
shareIntent,
context.getString(R.string.share_settle_title, projectName)
context.getString(R.string.title_settle)
)
context.startActivity(chooserIntent)
}
@@ -98,6 +99,10 @@ object BillsListUtils {
context: Context
) {
val timestamp = System.currentTimeMillis() / 1000
val proj = db.getProject(projectId)
val categories = db.getCategories(projectId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projectId, proj?.remoteId)
for (t in transactions) {
val owerId = t.owerMemberId
val receiverId = t.receiverMemberId
@@ -106,7 +111,7 @@ object BillsListUtils {
0, 0, projectId, owerId, amount,
timestamp, context.getString(R.string.settle_bill_what),
DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE,
DBBill.PAYMODE_NONE, reimbursementCategoryId,
"", DBBill.PAYMODE_ID_NONE
)
bill.billOwers += DBBillOwer(0, 0, receiverId)
@@ -1,12 +1,10 @@
package net.helcel.cowspent.android.main
import android.Manifest
import android.app.SearchManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
@@ -23,13 +21,9 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.Sync
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.nextcloud.android.sso.helper.SingleAccountHelper
@@ -54,6 +48,7 @@ import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillFormatter
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.CospendClientUtil
import net.helcel.cowspent.util.ExportUtil
import net.helcel.cowspent.util.ICallback
@@ -119,18 +114,8 @@ class BillsListViewActivity :
Log.d(TAG, "CREATED project id: $pid")
lifecycleScope.launch {
val addedProj = withContext(Dispatchers.IO) { db.getProject(pid) }
val message: String
val title: String
if (created) {
Log.e(TAG, "CREATED !!!")
title = getString(R.string.project_create_success_title)
message = getString(R.string.project_create_success_message, addedProj?.remoteId)
} else {
Log.e(TAG, "ADDED !!!")
title = getString(R.string.project_add_success_title)
message = getString(R.string.project_add_success_message, addedProj?.remoteId)
}
showDialog(message, title, Icons.Default.AddCircleOutline)
val message = getString(R.string.msg_project_added, addedProj?.name?.ifEmpty { addedProj.remoteId } ?: pid.toString())
showToast(this@BillsListViewActivity, message)
}
}
}
@@ -146,7 +131,7 @@ class BillsListViewActivity :
}
if (!db.cowspentServerSyncHelper.isSyncPossible) {
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
Toast.makeText(applicationContext, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG).show()
showToast(this@BillsListViewActivity, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG)
}
}
}
@@ -225,7 +210,7 @@ class BillsListViewActivity :
lifecycleScope.launch {
val members = withContext(Dispatchers.IO) { db.getActivatedMembersOfProject(selectedProjectId) }
if (members.isEmpty()) {
showToast(this@BillsListViewActivity, getString(R.string.add_bill_impossible_no_member))
showToast(this@BillsListViewActivity, getString(R.string.error_no_members))
} else {
val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) }
val createIntent = Intent(applicationContext, EditBillActivity::class.java).apply {
@@ -259,13 +244,23 @@ class BillsListViewActivity :
onProjectAction = { pid, actionIndex ->
when (actionIndex) {
0 -> onEditProjectClick(pid)
1 -> onRemoveProjectClick(pid)
1 -> {
val proj = db.getProject(pid)
if (proj?.type == ProjectType.COSPEND) {
onArchiveProjectClick(pid)
} else {
onRemoveProjectClick(pid)
}
}
2 -> onManageMembersClick(pid)
3 -> onManageCurrenciesClick(pid)
4 -> onProjectStatisticsClick(pid)
5 -> onSettleProjectClick(pid)
6 -> onShareProjectClick(pid)
7 -> onExportProjectClick(pid)
8 -> {
startActivity(net.helcel.cowspent.android.label.LabelManagementActivity.createIntent(this@BillsListViewActivity, pid))
}
}
},
onAccountSwitcherClick = {
@@ -387,17 +382,10 @@ class BillsListViewActivity :
private fun onEditProjectClick(projectId: Long) {
if (projectId == 0L) return
lifecycleScope.launch {
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
if (proj?.isLocal == false) {
val intent = Intent(applicationContext, EditProjectActivity::class.java).apply {
putExtra(EditProjectActivity.PARAM_PROJECT_ID, projectId)
}
editProjectLauncher.launch(intent)
} else {
showToast(this@BillsListViewActivity, getString(R.string.edit_project_local_impossible))
}
val intent = Intent(applicationContext, EditProjectActivity::class.java).apply {
putExtra(EditProjectActivity.PARAM_PROJECT_ID, projectId)
}
editProjectLauncher.launch(intent)
}
private fun onRemoveProjectClick(projectId: Long) {
@@ -406,8 +394,8 @@ class BillsListViewActivity :
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
viewModel.showDialog(
title = getString(R.string.confirm_remove_project_dialog_title),
message = if (!proj.isLocal) getString(R.string.confirm_remove_project_dialog_message) else null,
title = getString(R.string.title_confirm),
message = if (!proj.isLocal) getString(R.string.dialog_confirm_remove_project_msg) else null,
positiveText = getString(R.string.simple_yes),
onConfirm = {
lifecycleScope.launch {
@@ -428,6 +416,38 @@ class BillsListViewActivity :
}
}
private fun onArchiveProjectClick(projectId: Long) {
if (projectId == 0L) return
lifecycleScope.launch {
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
val isArchiving = !proj.isArchived
val newArchivedTs = if (isArchiving) System.currentTimeMillis() / 1000 else 0L
withContext(Dispatchers.IO) {
db.updateProject(
projId = projectId,
newName = null,
newEmail = null,
newPassword = null,
newLastPayerId = null,
newLastSyncedTimestamp = null,
newCurrencyName = null,
newDeletionDisabled = null,
newMyAccessLevel = null,
newBearerToken = null,
newArchivedTs = newArchivedTs
)
}
setupDrawerProjects()
refreshLists()
val msgRes = if (isArchiving) R.string.action_archive else R.string.action_unarchive
showToast(this@BillsListViewActivity, getString(msgRes))
}
}
fun onManageMembersClick(projectId: Long) {
if (projectId == 0L) return
lifecycleScope.launch {
@@ -437,18 +457,18 @@ class BillsListViewActivity :
return@launch
}
viewModel.showMemberManagementDialogByProjectId = projectId
startActivity(net.helcel.cowspent.android.project.member.MemberManagementActivity.createIntent(this@BillsListViewActivity, projectId))
}
}
fun onManageCurrenciesClick(projectId: Long) {
lifecycleScope.launch {
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
if (proj != null && proj.type == ProjectType.COSPEND) {
if (proj != null) {
startActivity(Intent(applicationContext, ManageCurrenciesActivity::class.java).apply {
putExtra(ManageCurrenciesActivity.EXTRA_PROJECT_ID, projectId)
})
} else showToast(this@BillsListViewActivity, getString(R.string.currency_management_unavailable))
}
}
}
@@ -464,7 +484,7 @@ class BillsListViewActivity :
lifecycleScope.launch {
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
if (projectId != 0L && proj?.isShareable() == true) viewModel.showShareDialogByProjectId = projectId
else showToast(this@BillsListViewActivity, getString(R.string.share_impossible), Toast.LENGTH_LONG)
else showToast(this@BillsListViewActivity, getString(R.string.error_share_impossible), Toast.LENGTH_LONG)
}
}
@@ -534,11 +554,13 @@ class BillsListViewActivity :
val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, proj.id, proj.remoteId)
val balances = HashMap<Long, Double>()
SupportUtil.getStats(
members, bills,
mutableMapOf(), balances, mutableMapOf(), mutableMapOf(),
-1000, -1000, null, null
-1000L, -1000L, reimbursementCategoryId, null, null
)
Triple(proj, members, balances)
}
@@ -565,7 +587,7 @@ class BillsListViewActivity :
} else {
val lastSyncTimestamp = proj.lastSyncedTimestamp ?: 0
val cal = Calendar.getInstance().apply { timeInMillis = lastSyncTimestamp * 1000 }
val text = getString(R.string.drawer_last_sync_text, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
val text = getString(R.string.drawer_last_sync, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
viewModel.lastSyncText = text
}
}
@@ -605,13 +627,13 @@ class BillsListViewActivity :
mid == null || mid == it.payerId || it.billOwersIds.contains(mid)
}
viewModel.hasUnlabeledBills = bills.any { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED }
viewModel.hasUnlabeledBills = bills.any { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val projectMembers = db.getMembersOfProject(projId, null)
val memberMap = projectMembers.associateBy { it.id }
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.remoteId }
val projectCategories = db.getCategories(projId).associateBy { it.remoteId }
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.id }
val projectCategories = db.getCategories(projId).associateBy { it.id }
BillFormatter.formatBills(
bills,
@@ -663,7 +685,7 @@ class BillsListViewActivity :
lifecycleScope.launch {
val members = withContext(Dispatchers.IO) { db.getActivatedMembersOfProject(selectedProjectId) }
if (members.isEmpty()) {
showToast(this@BillsListViewActivity, getString(R.string.add_bill_impossible_no_member))
showToast(this@BillsListViewActivity, getString(R.string.error_no_member))
} else {
val projType = withContext(Dispatchers.IO) { db.getProject(selectedProjectId)?.type?.id }
val intent = Intent(applicationContext, EditBillActivity::class.java).apply {
@@ -677,15 +699,6 @@ class BillsListViewActivity :
}
}
private fun showDialog(msg: String, title: String, icon: ImageVector) {
viewModel.showDialog(
title = title,
message = msg,
positiveText = getString(android.R.string.ok),
icon = icon
)
}
private fun updateUsernameInDrawer() {
if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) {
val text = getString(R.string.drawer_no_account)
@@ -777,8 +790,8 @@ class BillsListViewActivity :
lifecycleScope.launch {
val project = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
viewModel.showDialog(
title = getString(R.string.sync_error_dialog_title),
message = getString(R.string.sync_error_dialog_full_content, project.name, errorMessage),
title = getString(R.string.dialog_sync_error_title),
message = getString(R.string.dialog_sync_error_msg, project.name, errorMessage),
positiveText = getString(R.string.simple_close),
icon = Icons.Default.Sync
)
@@ -798,7 +811,7 @@ class BillsListViewActivity :
}
MainConstants.BROADCAST_SSO_TOKEN_MISMATCH -> {
viewModel.showDialog(
title = getString(R.string.sync_error_dialog_title),
title = getString(R.string.dialog_sync_error_title),
message = getString(R.string.error_token_mismatch),
positiveText = getString(R.string.simple_close),
icon = Icons.Default.Sync
@@ -118,8 +118,8 @@ object ProjectImportHelper {
val payerWeight = if (columns.containsKey("payer_weight")) line[columns["payer_weight"]!!].toDouble() else 1.0
val owersStr = if (columns.containsKey("owers")) line[columns["owers"]!!] else ""
val payerActive = columns.containsKey("payer_active") && line[columns["payer_active"]!!] == "1"
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toInt() else 0
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toInt() else 0
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toLong() else 0L
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toLong() else 0L
val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null
membersActive[payerName] = payerActive
@@ -150,8 +150,16 @@ object ProjectImportHelper {
val memberNameToId = mutableMapOf<String, Long>()
val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null))
paymentModes.forEach { db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color)) }
categories.forEach { db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color)) }
val pmRemoteToLocal = mutableMapOf<Long, Long>()
paymentModes.forEach {
val newId = db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color))
pmRemoteToLocal[it.remoteId] = newId
}
val catRemoteToLocal = mutableMapOf<Long, Long>()
categories.forEach {
val newId = db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color))
catRemoteToLocal[it.remoteId] = newId
}
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
membersWeight.keys.forEach { mName ->
@@ -160,7 +168,9 @@ object ProjectImportHelper {
bills.forEach { b ->
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, b.categoryRemoteId, b.comment, b.paymentModeRemoteId))
val localCatId = catRemoteToLocal[b.categoryId] ?: 0L
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId))
billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower ->
memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) }
}
@@ -4,19 +4,21 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
@Composable
fun ProjectOptionsDialogContent(
@@ -24,14 +26,17 @@ fun ProjectOptionsDialogContent(
onRemoveProject: () -> Unit,
onManageMembers: () -> Unit,
onManageCurrencies: () -> Unit,
onManageLabels: () -> Unit,
onStatistics: () -> Unit,
onSettle: () -> Unit,
onShareProject: () -> Unit,
onExportProject: () -> Unit,
onDismiss: () -> Unit,
isArchived: Boolean = false,
projectType: ProjectType = ProjectType.LOCAL,
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
isShareable: Boolean = true
isShareable: Boolean = true,
showBetaFeatures: Boolean = false
) {
Surface(
shape = MaterialTheme.shapes.large,
@@ -44,46 +49,71 @@ fun ProjectOptionsDialogContent(
.fillMaxWidth()
) {
Text(
text = stringResource(R.string.choose_project_management_action),
style = MaterialTheme.typography.h6,
text = stringResource(R.string.choose_project_management_action).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 8.dp)
)
val options = mutableListOf<ProjectOption>()
val isMaintainer = accessLevel >= DBProject.ACCESS_LEVEL_MAINTAINER || accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN
val isParticipant = accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT || accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN
val row1 = mutableListOf<ProjectOption>()
val row2 = mutableListOf<ProjectOption>()
val row3 = mutableListOf<ProjectOption>()
if (!isArchived && isMaintainer) {
options.add(ProjectOption(stringResource(R.string.action_edit_project), Icons.Default.Edit, onEditProject))
}
options.add(ProjectOption(stringResource(R.string.fab_rm_project), Icons.Default.Delete, onRemoveProject))
if (!isArchived && isMaintainer) {
options.add(ProjectOption(stringResource(R.string.fab_manage_members), Icons.Default.Group, onManageMembers))
options.add(ProjectOption(stringResource(R.string.fab_manage_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
}
options.add(ProjectOption(stringResource(R.string.fab_statistics), Icons.Default.BarChart, onStatistics))
if (!isArchived && isParticipant) {
options.add(ProjectOption(stringResource(R.string.fab_settle), Icons.Default.Handshake, onSettle))
row1.add(ProjectOption(stringResource(R.string.action_edit), Icons.Default.Edit, onEditProject))
}
if (isShareable && isParticipant) {
options.add(ProjectOption(stringResource(R.string.action_share_project), Icons.Default.Share, onShareProject))
row1.add(ProjectOption(stringResource(R.string.action_share), Icons.Default.Share, onShareProject))
}
if (projectType == ProjectType.COSPEND) {
val archiveLabel = if (isArchived) stringResource(R.string.action_unarchive) else stringResource(R.string.action_archive)
val archiveIcon = if (isArchived) Icons.Default.Unarchive else Icons.Default.Archive
row1.add(ProjectOption(archiveLabel, archiveIcon, onRemoveProject))
} else {
row1.add(ProjectOption(stringResource(R.string.action_delete), Icons.Default.Delete, onRemoveProject))
}
options.add(ProjectOption(stringResource(R.string.fab_export_project), Icons.Default.Download, onExportProject))
// Simple 2-column grid using Rows
for (i in options.indices step 2) {
Row(modifier = Modifier.fillMaxWidth()) {
ProjectOptionItem(
option = options[i],
modifier = Modifier.weight(1f)
)
if (i + 1 < options.size) {
// Row 2: Manage Member, Manage Labels, Manage Currencies
if (!isArchived && isMaintainer) {
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
if (showBetaFeatures && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels))
}
row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
}
// Row 3: Statistics, Settle, Export
row3.add(ProjectOption(stringResource(R.string.action_stats), Icons.Default.BarChart, onStatistics))
if (!isArchived && isParticipant) {
row3.add(ProjectOption(stringResource(R.string.action_settle), Icons.Default.Handshake, onSettle))
}
row3.add(ProjectOption(stringResource(R.string.action_export), Icons.Default.Download, onExportProject))
val allRows = listOf(row1, row2, row3).filter { it.isNotEmpty() }
allRows.forEach { rowOptions ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
val emptySpace = 3 - rowOptions.size
if (emptySpace > 0) {
Spacer(modifier = Modifier.weight(emptySpace / 2f))
}
rowOptions.forEach { option ->
ProjectOptionItem(
option = options[i + 1],
option = option,
modifier = Modifier.weight(1f)
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
if (emptySpace > 0) {
Spacer(modifier = Modifier.weight(emptySpace / 2f))
}
}
}
@@ -145,12 +175,64 @@ fun ProjectOptionsDialogPreview() {
onManageCurrencies = {},
onStatistics = {},
onSettle = {},
onManageLabels = {},
onShareProject = {},
onExportProject = {},
onDismiss = {},
isArchived = false,
projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true
isShareable = true,
showBetaFeatures = true
)
}
}
@Preview(showBackground = true)
@Composable
fun ProjectOptionsDialogPreview2() {
MaterialTheme {
ProjectOptionsDialogContent(
onEditProject = {},
onRemoveProject = {},
onManageMembers = {},
onManageCurrencies = {},
onStatistics = {},
onSettle = {},
onManageLabels = {},
onShareProject = {},
onExportProject = {},
onDismiss = {},
isArchived = true,
projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true,
showBetaFeatures = true
)
}
}
@Preview(showBackground = true)
@Composable
fun ProjectOptionsDialogPreview3() {
MaterialTheme {
ProjectOptionsDialogContent(
onEditProject = {},
onRemoveProject = {},
onManageMembers = {},
onManageCurrencies = {},
onStatistics = {},
onSettle = {},
onManageLabels = {},
onShareProject = {},
onExportProject = {},
onDismiss = {},
isArchived = false,
projectType = ProjectType.LOCAL,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true,
showBetaFeatures = true
)
}
}
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@@ -31,8 +32,6 @@ import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -42,10 +41,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
@@ -69,7 +66,7 @@ fun ProjectShareDialogContent(
) {
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
val qrCodeLinkWarn = stringResource(R.string.qrcode_link_open_attempt_warning)
val qrCodeLinkWarn = stringResource(R.string.msg_share_qr_warn)
val shareUrl = remember { proj.getShareUrl() }
val publicWebUrl = remember { proj.getPublicWebUrl() }
@@ -93,35 +90,24 @@ fun ProjectShareDialogContent(
.heightIn(max = 650.dp)
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 8.dp)
) {
Icon(
Icons.Default.Share,
contentDescription = null,
tint = MaterialTheme.colors.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.share_dialog_title),
style = MaterialTheme.typography.h6,
fontWeight = FontWeight.Bold
)
}
Text(
text = stringResource(R.string.title_share).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 16.dp)
)
Column(
modifier = Modifier
.weight(1f, fill = false)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
ShareCard(
title = stringResource(R.string.share_project_public_url_title),
title = stringResource(R.string.title_share_web),
url = publicWebUrl,
description = stringResource(R.string.share_project_public_url_dialog_message),
icon = Icons.Default.Link,
description = stringResource(R.string.msg_share_web),
onUrlClick = {
val i = Intent(Intent.ACTION_VIEW).apply {
data = publicWebUrl.toUri()
@@ -130,22 +116,21 @@ fun ProjectShareDialogContent(
},
onCopyClick = {
clipboardManager.setText(AnnotatedString(publicWebUrl))
Toast.makeText(context, "Link copied to clipboard", Toast.LENGTH_SHORT).show()
Toast.makeText(context, R.string.msg_link_copied, Toast.LENGTH_SHORT).show()
}
)
ShareCard(
title = stringResource(R.string.share_project_public_qrcode_title),
title = stringResource(R.string.title_share_qr),
url = shareUrl,
description = stringResource(R.string.share_project_dialog_message),
icon = Icons.Default.QrCode,
description = stringResource(R.string.msg_share_qr),
qrBitmap = qrBitmap,
onUrlClick = {
Toast.makeText(context, qrCodeLinkWarn, Toast.LENGTH_SHORT).show()
},
onCopyClick = {
clipboardManager.setText(AnnotatedString(shareUrl))
Toast.makeText(context, "Link copied to clipboard", Toast.LENGTH_SHORT).show()
Toast.makeText(context, R.string.msg_link_copied, Toast.LENGTH_SHORT).show()
}
)
}
@@ -165,43 +150,31 @@ private fun ShareCard(
title: String,
url: String,
description: String,
icon: ImageVector,
onUrlClick: () -> Unit,
onCopyClick: () -> Unit,
qrBitmap: Bitmap? = null
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = colorResource(R.color.fg_default_low).copy(alpha = 0.04f),
Column(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = title,
style = MaterialTheme.typography.subtitle2,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.8f)
)
}
Text(
text = title.uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 8.dp)
)
if (qrBitmap != null) {
Spacer(modifier = Modifier.height(8.dp))
if (qrBitmap != null) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier.size(128.dp)
modifier = Modifier
.size(128.dp)
.clip(MaterialTheme.shapes.small)
.background(Color.White),
contentAlignment = Alignment.Center
@@ -209,55 +182,53 @@ private fun ShareCard(
Image(
bitmap = qrBitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxSize()
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colors.surface,
modifier = Modifier.fillMaxWidth()
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.05f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.clickable { onUrlClick() }
.padding(start = 12.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.clickable { onUrlClick() }
.padding(start = 12.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = url,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.primary,
style = MaterialTheme.typography.body2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Medium
Text(
text = url,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.primary,
style = MaterialTheme.typography.body2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Medium
)
IconButton(onClick = onCopyClick) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy",
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
)
IconButton(onClick = onCopyClick) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy",
modifier = Modifier.size(18.dp),
tint = colorResource(R.color.fg_default_low)
)
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(4.dp))
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = description,
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
lineHeight = 16.sp,
modifier = Modifier.padding(horizontal = 8.dp)
)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = description,
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Start,
lineHeight = 16.sp,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
}
@@ -291,7 +262,7 @@ private fun DialogActions(
Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.simple_share_share).uppercase(),
text = stringResource(R.string.action_share).uppercase(),
style = MaterialTheme.typography.button,
fontWeight = FontWeight.Bold
)
@@ -21,6 +21,7 @@ import net.helcel.cowspent.model.*
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.*
import java.util.Locale
import androidx.core.net.toUri
import androidx.core.content.edit
import net.helcel.cowspent.model.ProjectType
@@ -60,7 +61,8 @@ class NewProjectActivity : AppCompatActivity() {
chooseFromNextcloud()
},
onOkPressed = { onPressOk() },
onBack = { finish() }
onBack = { finish() },
onFieldsChanged = { updateAuthStatus() }
)
}
}
@@ -86,6 +88,8 @@ class NewProjectActivity : AppCompatActivity() {
}
}
updateAuthStatus()
val defaultProjectId = intent.getStringExtra(PARAM_DEFAULT_PROJECT_ID)
if (defaultProjectId != null) {
viewModel.projectId = defaultProjectId
@@ -139,6 +143,17 @@ class NewProjectActivity : AppCompatActivity() {
}
}
private fun updateAuthStatus() {
val url = getFormattedUrl()
val fakeProj = DBProject(
0, "", "", "", url,
"", 0L, viewModel.projectType, 0L,
null, false, DBProject.ACCESS_LEVEL_UNKNOWN,
""
)
viewModel.isAuthenticatedAccount = db.cowspentServerSyncHelper.canCreateAuthenticatedProject(fakeProj)
}
private fun onPressOk() {
val type = viewModel.projectType
val todoCreate = viewModel.whatTodoIsCreate
@@ -150,15 +165,7 @@ class NewProjectActivity : AppCompatActivity() {
null, false, DBProject.ACCESS_LEVEL_UNKNOWN,
""
)
if (isValidUrl(url) && todoCreate && ProjectType.COSPEND == type &&
db.cowspentServerSyncHelper.canCreateAuthenticatedProject(fakeProj) &&
!viewModel.showAuthWarningDialog // Avoid infinite loop
) {
viewModel.showAuthWarningDialog = true
} else {
createProject()
}
createProject()
}
private fun getFormattedUrl(): String {
@@ -186,15 +193,24 @@ class NewProjectActivity : AppCompatActivity() {
}
private fun createProject() {
val isCospendScheme = isCospendSchemeLink(getFormattedUrl())
val url = getFormattedUrl()
val isCospendScheme = isCospendSchemeLink(url)
if (viewModel.whatTodoIsCreate && viewModel.projectId.isEmpty() && viewModel.projectName.isNotEmpty()) {
viewModel.projectId = viewModel.projectName.lowercase(Locale.ROOT)
.replace("[^a-z0-9]".toRegex(), "-")
.replace("-+".toRegex(), "-")
.trim('-')
}
val rid = viewModel.projectId
if (!isCospendScheme && (rid == "" || rid.contains(",") || rid.contains("/"))) {
showToast(getString(R.string.error_invalid_project_remote_id), Toast.LENGTH_LONG)
showToast(getString(R.string.error_invalid_project_id), Toast.LENGTH_LONG)
return
}
if (viewModel.projectType != ProjectType.LOCAL && !isCospendScheme) {
if (!isValidUrl(getFormattedUrl())) {
if (!isValidUrl(url)) {
showToast("Invalid URL", Toast.LENGTH_SHORT)
return
}
@@ -217,7 +233,16 @@ class NewProjectActivity : AppCompatActivity() {
showToast("Invalid project name", Toast.LENGTH_SHORT)
return
}
if (!SupportUtil.isValidEmail(viewModel.projectEmail)) {
val fakeProj = DBProject(
0, rid, "", viewModel.projectName, url,
viewModel.projectEmail, 0L, viewModel.projectType, 0L,
null, false, DBProject.ACCESS_LEVEL_UNKNOWN,
""
)
val isAuthenticated = db.cowspentServerSyncHelper.canCreateAuthenticatedProject(fakeProj)
if (!isAuthenticated && !SupportUtil.isValidEmail(viewModel.projectEmail)) {
showToast("Invalid email", Toast.LENGTH_SHORT)
return
}
@@ -226,7 +251,7 @@ class NewProjectActivity : AppCompatActivity() {
if (!db.cowspentServerSyncHelper.createRemoteProject(
viewModel.projectId, viewModel.projectName,
viewModel.projectEmail, viewModel.projectPassword, getFormattedUrl(), viewModel.projectType, createRemoteCallBack
viewModel.projectEmail, viewModel.projectPassword, url, viewModel.projectType, createRemoteCallBack
)
) {
viewModel.isCreatingRemoteProject = false
@@ -287,7 +312,6 @@ class NewProjectActivity : AppCompatActivity() {
pid
)
}
showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG)
return pid
}
@@ -19,6 +19,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -32,12 +33,17 @@ fun NewProjectScreen(
onImportFile: () -> Unit,
onChooseFromNextcloud: () -> Unit,
onOkPressed: () -> Unit,
onBack: () -> Unit
onBack: () -> Unit,
onFieldsChanged: () -> Unit
) {
LaunchedEffect(viewModel.projectUrl, viewModel.projectType) {
onFieldsChanged()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.action_add_project)) },
title = { Text(stringResource(R.string.title_add_project)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -50,7 +56,7 @@ fun NewProjectScreen(
floatingActionButton = {
if (viewModel.isFormValid()) {
FloatingActionButton(onClick = onOkPressed) {
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save_bill))
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save))
}
}
}
@@ -58,17 +64,18 @@ fun NewProjectScreen(
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.verticalScroll(rememberScrollState())
.fillMaxSize()
) {
// What to do
SectionRow(
label = stringResource(R.string.new_project_what_todo)
label = stringResource(R.string.new_project_action)
) {
Row {
ToggleButton(
text = stringResource(R.string.todo_join_label),
text = stringResource(R.string.todo_join),
selected = !viewModel.whatTodoIsCreate,
onClick = {
viewModel.whatTodoIsCreate = false
@@ -78,7 +85,7 @@ fun NewProjectScreen(
)
Spacer(modifier = Modifier.width(8.dp))
ToggleButton(
text = stringResource(R.string.todo_create_label),
text = stringResource(R.string.todo_create),
selected = viewModel.whatTodoIsCreate,
onClick = { viewModel.whatTodoIsCreate = true }
)
@@ -93,20 +100,20 @@ fun NewProjectScreen(
Row {
if (viewModel.whatTodoIsCreate) {
ToggleButton(
text = stringResource(R.string.where_local_short),
text = stringResource(R.string.where_local),
selected = viewModel.projectType == ProjectType.LOCAL,
onClick = { viewModel.projectType = ProjectType.LOCAL }
)
Spacer(modifier = Modifier.width(8.dp))
}
ToggleButton(
text = stringResource(R.string.where_cospend_short),
text = stringResource(R.string.where_cospend),
selected = viewModel.projectType == ProjectType.COSPEND,
onClick = { viewModel.projectType = ProjectType.COSPEND }
)
Spacer(modifier = Modifier.width(8.dp))
ToggleButton(
text = stringResource(R.string.where_ihatemoney_short),
text = stringResource(R.string.where_ihatemoney),
selected = viewModel.projectType == ProjectType.IHATEMONEY,
onClick = { viewModel.projectType = ProjectType.IHATEMONEY }
)
@@ -115,13 +122,7 @@ fun NewProjectScreen(
Spacer(modifier = Modifier.height(8.dp))
if (viewModel.whatTodoIsCreate) {
if (viewModel.projectType == ProjectType.COSPEND) {
Button(
onClick = onChooseFromNextcloud
) {
Text(stringResource(R.string.new_project_from_nextcloud_tooltip))
}
} else if (viewModel.projectType == ProjectType.LOCAL) {
if (viewModel.projectType == ProjectType.LOCAL) {
Button(
onClick = onImportFile
) {
@@ -131,7 +132,7 @@ fun NewProjectScreen(
} else {
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = onScanQrCode) {
Text(text = stringResource(R.string.scan_qrcode))
Text(text = stringResource(R.string.action_scan_qrcode))
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.QrCode2,
@@ -148,13 +149,8 @@ fun NewProjectScreen(
OutlinedTextField(
value = viewModel.projectUrl,
onValueChange = { viewModel.projectUrl = it },
label = {
Text(
stringResource(
if (viewModel.projectType == ProjectType.COSPEND) R.string.setting_cospend_project_url
else R.string.setting_ihatemoney_project_url
)
)
placeholder = {
Text(stringResource(R.string.label_url))
},
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Link, contentDescription = null) }
@@ -162,42 +158,47 @@ fun NewProjectScreen(
Spacer(modifier = Modifier.height(8.dp))
}
OutlinedTextField(
value = viewModel.projectId,
onValueChange = { viewModel.projectId = it },
label = { Text(stringResource(R.string.setting_project_id)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null) }
)
if (!viewModel.whatTodoIsCreate || !viewModel.isAuthenticatedAccount || viewModel.projectType != ProjectType.COSPEND) {
OutlinedTextField(
value = viewModel.projectId,
onValueChange = { viewModel.projectId = it },
placeholder = { Text(stringResource(R.string.label_project_id)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
}
if (viewModel.projectType != ProjectType.LOCAL && (!viewModel.whatTodoIsCreate || viewModel.projectType == ProjectType.IHATEMONEY)) {
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.projectPassword,
onValueChange = { viewModel.projectPassword = it },
label = { Text(stringResource(R.string.setting_new_project_password)) },
placeholder = { Text(stringResource(R.string.label_password)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
}
if (viewModel.whatTodoIsCreate && viewModel.projectType != ProjectType.LOCAL) {
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.projectName,
onValueChange = { viewModel.projectName = it },
label = { Text(stringResource(R.string.setting_new_project_name)) },
placeholder = { Text(stringResource(R.string.label_project_title)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.projectEmail,
onValueChange = { viewModel.projectEmail = it },
label = { Text(stringResource(R.string.setting_new_project_email)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
)
if (!viewModel.isAuthenticatedAccount || viewModel.projectType == ProjectType.IHATEMONEY) {
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.projectEmail,
onValueChange = { viewModel.projectEmail = it },
placeholder = { Text(stringResource(R.string.label_email)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
)
}
}
}
}
@@ -205,8 +206,8 @@ fun NewProjectScreen(
if (viewModel.showAuthWarningDialog) {
AlertDialog(
onDismissRequest = { viewModel.showAuthWarningDialog = false },
title = { Text(stringResource(R.string.auth_project_creation_title)) },
text = { Text(stringResource(R.string.warning_auth_project_creation)) },
title = { Text(stringResource(R.string.app_name)) },
text = { Text(stringResource(R.string.msg_auth_warning)) },
confirmButton = {
TextButton(onClick = {
viewModel.showAuthWarningDialog = false
@@ -260,12 +261,12 @@ fun NewProjectScreen(
if (viewModel.isCreatingRemoteProject) {
AlertDialog(
onDismissRequest = { },
title = { Text(stringResource(R.string.simple_loading)) },
title = { Text(stringResource(R.string.error_loading)) },
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator()
Spacer(modifier = Modifier.width(16.dp))
Text(stringResource(R.string.creating_remote_project))
Text(stringResource(R.string.action_add_project))
}
},
confirmButton = {}
@@ -275,7 +276,7 @@ fun NewProjectScreen(
if (viewModel.errorDialogMessage != null) {
AlertDialog(
onDismissRequest = { viewModel.errorDialogMessage = null },
title = { Text(stringResource(R.string.simple_error)) },
title = { Text(stringResource(R.string.error_generic)) },
text = { Text(viewModel.errorDialogMessage!!) },
confirmButton = {
TextButton(onClick = { viewModel.errorDialogMessage = null }) {
@@ -291,11 +292,15 @@ fun SectionRow(
label: String,
content: @Composable () -> Unit
) {
Row(verticalAlignment = Alignment.Top) {
Column {
Text(label, fontSize = 12.sp, color = MaterialTheme.colors.onSurface)
content()
}
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
Text(
text = label.uppercase(),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
content()
}
}
@@ -334,6 +339,7 @@ fun NewProjectScreenPreview() {
onImportFile = {},
onChooseFromNextcloud = {},
onOkPressed = {},
onBack = {}
onBack = {},
onFieldsChanged = {}
)
}
@@ -30,6 +30,8 @@ class NewProjectViewModel : ViewModel() {
var projectName by mutableStateOf("")
var projectEmail by mutableStateOf("")
var isAuthenticatedAccount by mutableStateOf(false)
var showAuthWarningDialog by mutableStateOf(false)
var showNextcloudProjectDialog by mutableStateOf(false)
var nextcloudProjects by mutableStateOf<List<DBAccountProject>>(emptyList())
@@ -39,12 +41,23 @@ class NewProjectViewModel : ViewModel() {
fun isFormValid(): Boolean {
if (whatTodoIsCreate) {
if (projectId.isEmpty()) return false
if (projectType != ProjectType.LOCAL) {
if (projectType == ProjectType.LOCAL) {
return projectId.isNotEmpty()
} else {
if (projectUrl.isEmpty()) return false
if (projectName.isEmpty()) return false
if (projectEmail.isEmpty()) return false
if (projectType == ProjectType.IHATEMONEY && projectPassword.isEmpty()) return false
if (projectType == ProjectType.COSPEND && isAuthenticatedAccount) {
return true
}
if (projectId.isEmpty()) return false
if (projectType == ProjectType.IHATEMONEY) {
if (projectEmail.isEmpty()) return false
if (projectPassword.isEmpty()) return false
} else {
if (projectEmail.isEmpty()) return false
}
}
} else {
// Join
@@ -137,7 +137,7 @@ class EditProjectActivity : AppCompatActivity() {
private fun onDeleteRemote() {
viewModel.showDialog(
message = getString(R.string.confirm_delete_project_dialog_title),
message = getString(R.string.title_confirm),
positiveText = getString(R.string.simple_yes),
onConfirm = {
if (!db.cowspentServerSyncHelper.deleteRemoteProject(project.id, deleteCallBack)) {
@@ -2,6 +2,8 @@ package net.helcel.cowspent.android.project.edit
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -14,6 +16,7 @@ import androidx.compose.material.icons.filled.Title
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R
@@ -59,70 +62,91 @@ fun EditProjectScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.simple_edit_project)) },
title = { Text(stringResource(R.string.title_edit_project)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
actions = {
IconButton(onClick = onDeleteRemote) {
Icon(Icons.Default.DeleteForever, contentDescription = stringResource(R.string.menu_delete_project_remote))
if (!viewModel.isLocal) {
IconButton(onClick = onDeleteRemote) {
Icon(Icons.Default.DeleteForever, contentDescription = stringResource(R.string.action_delete))
}
}
},
backgroundColor = MaterialTheme.colors.surface,
backgroundColor = MaterialTheme.colors.primary,
elevation = 0.dp
)
},
floatingActionButton = {
FloatingActionButton(onClick = onSave) {
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.menu_save_project))
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save))
}
}
) { padding ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(padding)
.imePadding()
.padding(16.dp)
.fillMaxSize()
.verticalScroll(scrollState)
) {
Text(
text = "GENERAL",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField(
value = viewModel.name,
onValueChange = { viewModel.name = it },
label = { Text(stringResource(R.string.setting_new_project_name)) },
placeholder = { Text(stringResource(R.string.label_project_title)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
if (!viewModel.isLocal) {
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.password,
onValueChange = { viewModel.password = it },
label = { Text(stringResource(R.string.setting_password)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
)
OutlinedTextField(
value = viewModel.email,
onValueChange = { viewModel.email = it },
placeholder = { Text(stringResource(R.string.label_email)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "SECURITY",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp, top = 24.dp)
)
OutlinedTextField(
value = viewModel.newPassword,
onValueChange = { viewModel.newPassword = it },
label = { Text(stringResource(R.string.setting_new_project_password)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.LockOpen, contentDescription = null) }
)
OutlinedTextField(
value = viewModel.password,
onValueChange = { viewModel.password = it },
placeholder = { Text("Old " + stringResource(R.string.label_password)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.email,
onValueChange = { viewModel.email = it },
label = { Text(stringResource(R.string.setting_new_project_email)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
)
OutlinedTextField(
value = viewModel.newPassword,
onValueChange = { viewModel.newPassword = it },
placeholder = { Text("New " + stringResource(R.string.label_password)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.LockOpen, contentDescription = null) }
)
}
}
}
}
@@ -13,6 +13,7 @@ class EditProjectViewModel : ViewModel() {
var password by mutableStateOf("")
var newPassword by mutableStateOf("")
var email by mutableStateOf("")
var isLocal by mutableStateOf(false)
var dialogState by mutableStateOf<DialogState?>(null)
@@ -53,5 +54,6 @@ class EditProjectViewModel : ViewModel() {
password = project.password
newPassword = project.password
email = project.email?.let { if (it == "null") "" else it } ?: ""
isLocal = project.isLocal
}
}
@@ -6,6 +6,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview
@@ -29,15 +30,17 @@ fun MemberAddDialogContent(
.padding(16.dp)
) {
Text(
text = stringResource(R.string.add_member_dialog_title),
style = MaterialTheme.typography.h6,
text = stringResource(R.string.add_member_dialog_title).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text(stringResource(R.string.member_edit_name)) },
placeholder = { Text(stringResource(R.string.label_name)) },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
singleLine = true
@@ -13,6 +13,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
@@ -57,8 +58,10 @@ fun MemberEditDialogContent(
.verticalScroll(rememberScrollState())
) {
Text(
text = stringResource(R.string.edit_member_dialog_title),
style = MaterialTheme.typography.h6,
text = stringResource(R.string.edit_member_dialog_title).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 16.dp)
)
@@ -66,7 +69,7 @@ fun MemberEditDialogContent(
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text(stringResource(R.string.member_edit_name)) },
placeholder = { Text(stringResource(R.string.label_name)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null)
@@ -80,7 +83,7 @@ fun MemberEditDialogContent(
OutlinedTextField(
value = weight,
onValueChange = { weight = it },
label = { Text(stringResource(R.string.member_edit_weight)) },
placeholder = { Text(stringResource(R.string.label_weight)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(Icons.Default.LineWeight, contentDescription = null)
@@ -98,9 +101,17 @@ fun MemberEditDialogContent(
.clickable { isActivated = !isActivated }
.padding(vertical = 8.dp)
) {
Icon(Icons.Default.Block, contentDescription = null)
Spacer(modifier = Modifier.width(16.dp))
Text(stringResource(R.string.member_edit_toggle), modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.Default.Block,
contentDescription = null,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = stringResource(R.string.label_activated),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.subtitle1
)
Checkbox(checked = isActivated, onCheckedChange = { isActivated = it })
}
@@ -113,9 +124,17 @@ fun MemberEditDialogContent(
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Icon(Icons.Default.Palette, contentDescription = null)
Spacer(modifier = Modifier.width(16.dp))
Text(stringResource(R.string.member_edit_color), modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.Default.Palette,
contentDescription = null,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = stringResource(R.string.label_color),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.subtitle1
)
Box(
modifier = Modifier
.size(40.dp)
@@ -0,0 +1,122 @@
package net.helcel.cowspent.android.project.member
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import net.helcel.cowspent.R
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils
class MemberManagementActivity : AppCompatActivity() {
private val viewModel: MemberManagementViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, 0)
if (projectId == 0L) {
finish()
return
}
val db = CowspentSQLiteOpenHelper.getInstance(this)
viewModel.loadMembers(projectId)
setContent {
ThemeUtils.CowspentTheme {
var showAddMemberDialog by remember { mutableStateOf(false) }
var editingMember by remember { mutableStateOf<DBMember?>(null) }
MemberManagementScreen(
members = viewModel.members,
onAddMember = { showAddMemberDialog = true },
onEditMember = { editingMember = it },
onToggleMember = { member, isActivated ->
db.updateMemberAndSync(member, member.name, member.weight, isActivated, member.r, member.g, member.b, "", "")
viewModel.loadMembers(projectId)
},
onBack = { finish() }
)
if (showAddMemberDialog) {
Dialog(
onDismissRequest = { showAddMemberDialog = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
MemberAddDialogContent(
onAdd = { memberName ->
val memberNames = db.getMembersOfProject(projectId, null).map { it.name }
if (memberNames.contains(memberName)) {
Toast.makeText(this, R.string.member_already_exists, Toast.LENGTH_SHORT).show()
} else {
val color = net.helcel.cowspent.android.helper.TextDrawable.getColorFromName(memberName)
db.addMemberAndSync(
DBMember(
0, 0, projectId, memberName, true, 1.0,
DBBill.STATE_ADDED,
android.graphics.Color.red(color),
android.graphics.Color.green(color),
android.graphics.Color.blue(color),
null, null
)
)
viewModel.loadMembers(projectId)
showAddMemberDialog = false
}
},
onDismiss = { showAddMemberDialog = false }
)
}
}
if (editingMember != null) {
val member = editingMember!!
Dialog(
onDismissRequest = { editingMember = null },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
MemberEditDialogContent(
member = member,
onSave = { name, weight, isActivated, r, g, b ->
db.updateMemberAndSync(member, name, weight, isActivated, r, g, b, "", "")
viewModel.loadMembers(projectId)
editingMember = null
},
onDelete = {
db.deleteMember(member.id)
viewModel.loadMembers(projectId)
editingMember = null
},
onDismiss = { editingMember = null }
)
}
}
}
}
}
companion object {
const val EXTRA_PROJECT_ID = "project_id"
fun createIntent(context: Context, projectId: Long): Intent {
return Intent(context, MemberManagementActivity::class.java).apply {
putExtra(EXTRA_PROJECT_ID, projectId)
}
}
}
}
@@ -1,132 +0,0 @@
package net.helcel.cowspent.android.project.member
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
import net.helcel.cowspent.model.DBMember
@Composable
fun MemberManagementDialogContent(
members: List<DBMember>,
onAddMember: () -> Unit,
onEditMember: (DBMember) -> Unit,
onDismiss: () -> Unit
) {
val listState = rememberLazyListState()
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colors.surface,
contentColor = contentColorFor(MaterialTheme.colors.surface)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 500.dp)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.fab_manage_members),
style = MaterialTheme.typography.h6
)
IconButton(onClick = onAddMember) {
Icon(Icons.Default.Add,modifier=Modifier.size(32.dp),
contentDescription = stringResource(R.string.fab_add_member))
}
}
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f, fill = false)
.lazyVerticalScrollbar(listState)
) {
items(members) { member ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onEditMember(member) }
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
UserAvatar(
name = member.name,
r = member.r,
g = member.g,
b = member.b,
avatar = member.avatar,
disabled = !member.isActivated,
size = 40.dp
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = member.name,
style = MaterialTheme.typography.body1
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.simple_close).uppercase())
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun MemberManagementDialogContentPreview() {
MaterialTheme {
MemberManagementDialogContent(
members = listOf(
DBMember(1, 0, 1, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
DBMember(2, 0, 1, "Bob", true, 1.0, 0, 100, 255, 100, null, null),
DBMember(3, 0, 1, "Charlie", false, 1.0, 0, 100, 100, 255, null, null)
),
onAddMember = {},
onEditMember = {},
onDismiss = {}
)
}
}
@@ -0,0 +1,162 @@
package net.helcel.cowspent.android.project.member
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
import net.helcel.cowspent.model.DBMember
@Composable
fun MemberManagementScreen(
members: List<DBMember>,
onAddMember: () -> Unit,
onEditMember: (DBMember) -> Unit,
onToggleMember: (DBMember, Boolean) -> Unit,
onBack: () -> Unit
) {
val listState = rememberLazyListState()
val activatedMembers = members.filter { it.isActivated }
val deactivatedMembers = members.filter { !it.isActivated }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.action_members)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
backgroundColor = MaterialTheme.colors.primary,
elevation = 0.dp
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddMember) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_save))
}
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.lazyVerticalScrollbar(listState)
) {
item {
Text(
text = "ACTIVATED MEMBERS",
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(16.dp)
)
}
items(activatedMembers) { member ->
MemberRow(
member = member,
onEditMember = onEditMember,
onToggleMember = { onToggleMember(member, it) }
)
}
if (deactivatedMembers.isNotEmpty()) {
item {
Text(
text = "DEACTIVATED MEMBERS",
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 16.dp)
)
}
items(deactivatedMembers) { member ->
MemberRow(
member = member,
onEditMember = onEditMember,
onToggleMember = { onToggleMember(member, it) },
alpha = 0.6f
)
}
}
}
}
}
}
@Composable
fun MemberRow(
member: DBMember,
onEditMember: (DBMember) -> Unit,
onToggleMember: (Boolean) -> Unit,
alpha: Float = 1f
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onEditMember(member) }
.padding(horizontal = 16.dp, vertical = 0.dp),
verticalAlignment = Alignment.CenterVertically
) {
MemberAvatar(
member = member,
size = 24.dp,
alpha = alpha
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = member.name,
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.onSurface.copy(alpha = alpha)
)
IconButton(onClick = { onToggleMember(!member.isActivated) }) {
Icon(
imageVector = if (member.isActivated) Icons.Default.Delete else Icons.Default.Add,
contentDescription = if (member.isActivated) "Deactivate" else "Activate",
tint = if (member.isActivated) MaterialTheme.colors.error.copy(alpha = 0.6f) else MaterialTheme.colors.primary.copy(alpha = 0.6f)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun MemberManagementScreenPreview() {
MaterialTheme {
MemberManagementScreen(
members = listOf(
DBMember(1, 0, 1, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
DBMember(2, 0, 1, "Bob", true, 1.0, 0, 100, 255, 100, null, null),
DBMember(3, 0, 1, "Charlie", false, 1.0, 0, 100, 100, 255, null, null)
),
onAddMember = {},
onEditMember = {},
onToggleMember = { _, _ -> },
onBack = {}
)
}
}
@@ -0,0 +1,40 @@
package net.helcel.cowspent.android.project.member
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
class MemberManagementViewModel(application: Application) : AndroidViewModel(application) {
private val db = CowspentSQLiteOpenHelper.getInstance(application)
var projectId by mutableLongStateOf(0L)
var members by mutableStateOf<List<DBMember>>(emptyList())
var dialogState by mutableStateOf<DialogState?>(null)
fun loadMembers(projId: Long) {
projectId = projId
viewModelScope.launch {
members = withContext(Dispatchers.IO) { db.getMembersOfProject(projId, null) }
}
}
fun deleteMember(id: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
db.deleteMember(id)
}
loadMembers(projectId)
}
}
}
@@ -129,8 +129,10 @@ fun ProjectSettlementUI(
.padding(16.dp)
) {
Text(
text = stringResource(R.string.settle_dialog_title),
style = MaterialTheme.typography.h6,
text = stringResource(R.string.title_settle).uppercase(),
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
modifier = Modifier.padding(bottom = 16.dp)
)
@@ -174,7 +176,7 @@ private fun BalancedStateMessage() {
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.settle_dialog_balanced),
text = stringResource(R.string.dialog_balanced_msg),
style = MaterialTheme.typography.body1
)
}
@@ -279,9 +281,9 @@ private fun TableHeaderText(
Text(
text = text.uppercase(),
modifier = modifier,
fontWeight = FontWeight.SemiBold,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.overline,
color = colorResource(R.color.fg_default_low),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
textAlign = textAlign,
letterSpacing = 0.8.sp
)
@@ -79,6 +79,7 @@ fun SettingsScreen(
val keyColor = stringResource(R.string.pref_key_color)
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
val keyShowArchived = stringResource(R.string.pref_key_show_archived)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
// States for preferences
var nightMode by remember(keyNightMode) {
@@ -113,6 +114,9 @@ fun SettingsScreen(
var showArchived by remember(keyShowArchived) {
mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false))
}
var betaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
Scaffold(
topBar = {
@@ -135,10 +139,10 @@ fun SettingsScreen(
.verticalScroll(rememberScrollState())
) {
// Appearance
SettingsCategory(stringResource(R.string.settings_appearance_category))
SettingsCategory(stringResource(R.string.settings_appearance))
SettingsSwitchPreference(
title = stringResource(R.string.setting_show_archived),
title = stringResource(R.string.settings_show_archived),
icon = Icons.Default.Archive,
checked = showArchived,
onCheckedChange = {
@@ -160,7 +164,7 @@ fun SettingsScreen(
)
SettingsListPreference(
title = stringResource(R.string.settings_night_mode_title),
title = stringResource(R.string.settings_night_mode),
icon = Icons.Default.Brightness2,
value = nightMode,
entries = mapOf(
@@ -178,7 +182,7 @@ fun SettingsScreen(
)
SettingsListPreference(
title = stringResource(R.string.settings_color_mode_title),
title = stringResource(R.string.settings_color_mode),
icon = Icons.Default.Palette,
value = colorMode,
entries = mapOf(
@@ -203,8 +207,8 @@ fun SettingsScreen(
if (colorMode == "manual") {
SettingsColorPreference(
title = stringResource(R.string.settings_color_title),
summary = stringResource(R.string.settings_color_summary),
title = stringResource(R.string.settings_color_custom),
summary = stringResource(R.string.settings_color_custom),
icon = Icons.Default.Palette,
initialColor = appColor,
onColorSelected = {
@@ -218,10 +222,10 @@ fun SettingsScreen(
}
// Network
SettingsCategory(stringResource(R.string.settings_network_category))
SettingsCategory(stringResource(R.string.settings_network))
SettingsSwitchPreference(
title = stringResource(R.string.settings_offline_mode_title),
title = stringResource(R.string.settings_offline_mode),
summary = stringResource(R.string.settings_offline_mode_summary),
icon = Icons.Default.Sync,
checked = offlineMode,
@@ -234,16 +238,29 @@ fun SettingsScreen(
)
SettingsPreference(
title = stringResource(R.string.settings_server_settings),
title = stringResource(R.string.title_account),
icon = Icons.Default.AccountCircle,
onClick = onAccountSettingsClick
)
// Other
SettingsCategory(stringResource(R.string.settings_other_category))
SettingsCategory(stringResource(R.string.settings_other))
SettingsSwitchPreference(
title = stringResource(R.string.settings_beta_features),
summary = stringResource(R.string.settings_beta_features_summary),
icon = Icons.Default.Info,
checked = betaFeatures,
onCheckedChange = {
betaFeatures = it
sharedPreferences.edit {
putBoolean(keyBetaFeatures, it)
}
}
)
SettingsPreference(
title = stringResource(R.string.settings_about),
title = stringResource(R.string.title_about),
icon = Icons.Default.Info,
onClick = onAboutClick
)
@@ -255,9 +272,9 @@ fun SettingsScreen(
fun SettingsCategory(title: String) {
Text(
text = title.uppercase(),
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp),
color = MaterialTheme.colors.primary,
style = MaterialTheme.typography.overline,
modifier = Modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp),
color = MaterialTheme.colors.onSurface,
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold
)
}
@@ -66,11 +66,12 @@ import androidx.core.graphics.toColorInt
import kotlinx.coroutines.launch
import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.EditableExposedDropdownMenu
import net.helcel.cowspent.android.helper.UserAvatar
import net.helcel.cowspent.android.helper.MemberAvatar
import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.CategoryUtils
import kotlin.math.abs
import kotlin.math.roundToInt
@@ -87,38 +88,43 @@ private object SankeyDimens {
@Composable
fun ProjectSankeyDiagram(
projectName: String,
projectId: Long,
projectRemoteId: String?,
allMembers: List<DBMember>,
allBills: List<DBBill>,
customCategories: List<DBCategory>,
onShareReady: (String) -> Unit
) {
val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName)
val shareStatsIntro = stringResource(R.string.msg_stats_intro, projectName)
var selectedMemberId by remember { mutableLongStateOf(-1L) }
var expanded by remember { mutableStateOf(false) }
val activeBills = remember(allBills) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT }
val reimbursementCategoryId = remember(customCategories, projectId, projectRemoteId) {
CategoryUtils.getReimbursementCategoryId(customCategories, projectId, projectRemoteId)
}
val activeBills = remember(allBills, reimbursementCategoryId) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != reimbursementCategoryId }
}
val membersMap = remember(allMembers) { allMembers.associateBy { it.id } }
val categoriesMap = remember(customCategories) { customCategories.associateBy { it.remoteId.toInt() } }
val categoriesMap = remember(customCategories) { customCategories.associateBy { it.id } }
if (activeBills.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No data to display", style = MaterialTheme.typography.h6)
Text("No data to display", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
}
return
}
val spendings = remember(activeBills, selectedMemberId, membersMap) {
val spentMap = mutableMapOf<Long, Double>().apply { membersMap.keys.forEach { put(it, 0.0) } }
val catMap = mutableMapOf<Int, Double>()
val catMap = mutableMapOf<Long, Double>()
activeBills.forEach { bill ->
val totalWeight = bill.billOwers.sumOf { membersMap[it.memberId]?.weight ?: 1.0 }
if (totalWeight > 0) {
if (selectedMemberId == -1L) {
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + bill.amount
catMap[bill.categoryId] = (catMap[bill.categoryId] ?: 0.0) + bill.amount
bill.billOwers.forEach { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0
spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + (bill.amount / totalWeight) * weight
@@ -126,7 +132,7 @@ fun ProjectSankeyDiagram(
} else {
bill.billOwers.find { it.memberId == selectedMemberId }?.let { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + (bill.amount / totalWeight) * weight
catMap[bill.categoryId] = (catMap[bill.categoryId] ?: 0.0) + (bill.amount / totalWeight) * weight
spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + (bill.amount / totalWeight) * weight
}
}
@@ -170,7 +176,7 @@ fun ProjectSankeyDiagram(
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
if (selectedMember != null) {
UserAvatar(name = selectedMember.name, r = selectedMember.r, g = selectedMember.g, b = selectedMember.b, disabled = !selectedMember.isActivated, size = 24.dp)
MemberAvatar(member = selectedMember, size = 24.dp)
} else Icon(Icons.Default.Group, contentDescription = null)
}
},
@@ -182,7 +188,7 @@ fun ProjectSankeyDiagram(
}
allMembers.forEach { member ->
DropdownMenuItem(onClick = { selectedMemberId = member.id; expanded = false }) {
UserAvatar(name = member.name, r = member.r, g = member.g, b = member.b, disabled = !member.isActivated, size = 24.dp)
MemberAvatar(member = member, size = 24.dp)
Spacer(modifier = Modifier.width(12.dp))
Text(member.name)
}
@@ -215,9 +221,9 @@ private fun SankeyContent(
selectedMemberId: Long,
totalAmount: Double,
displayMemberSpendings: List<Pair<Long, Double>>,
displayCategorySpendings: List<Pair<Int, Double>>,
displayCategorySpendings: List<Pair<Long, Double>>,
membersMap: Map<Long, DBMember>,
categoriesMap: Map<Int, DBCategory>
categoriesMap: Map<Long, DBCategory>
) {
val density = LocalDensity.current
val scope = rememberCoroutineScope()
@@ -399,4 +405,4 @@ private fun DrawScope.drawSankeyFlow(startX: Float, startY: Float, startWidth: F
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", 1L, "1", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
@@ -43,10 +43,10 @@ fun ProjectSpendingGraph(
allBills: List<DBBill>,
onShareReady: (String) -> Unit
) {
val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName)
val shareStatsIntro = stringResource(R.string.msg_stats_intro, projectName)
if (allBills.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No data to display", style = MaterialTheme.typography.h6)
Text("No data to display", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
}
return
}
@@ -35,7 +35,7 @@ fun ProjectStatisticsScreen(
}
var currentShareText by remember { mutableStateOf("") }
val tabs = listOf(
stringResource(R.string.statistic_title),
stringResource(R.string.title_stats),
"Trend",
"Sankey"
)
@@ -43,7 +43,7 @@ fun ProjectStatisticsScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.statistic_title)) },
title = { Text(stringResource(R.string.title_stats)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -84,6 +84,21 @@ fun ProjectStatisticsScreen(
val statsData by produceState<StatisticsData?>(null, proj.id) {
value = withContext(Dispatchers.IO) {
val syncedCategories = db.getCategories(proj.id)
val defaultCategories = CategoryUtils.getDefaultCategories(context, proj.id)
val toEnsure = if (proj.type == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT })
}
toEnsure.forEach { def ->
if (syncedCategories.none { it.remoteId == def.remoteId }) {
db.addCategory(def)
}
}
val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
@@ -94,19 +109,12 @@ fun ProjectStatisticsScreen(
if (statsData != null) {
val data = statsData!!
val defaultCategories = remember(proj.id) { CategoryUtils.getDefaultCategories(context, proj.id) }
val categories = remember(proj.type, data.categories, defaultCategories) {
val hardcoded = if (proj.type == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
(data.categories + hardcoded).distinctBy { it.remoteId }
}
val categories = data.categories
val categoryNoneLabel = stringResource(R.string.category_none)
val sankeyCategories = remember(proj.id, data.categories, defaultCategories, categoryNoneLabel) {
val sankeyCategories = remember(proj.id, data.categories, categoryNoneLabel) {
val noneCategory = DBCategory(0, 0, proj.id, categoryNoneLabel, "", "#9E9E9E")
(data.categories + defaultCategories + noneCategory).distinctBy { it.remoteId }
(data.categories + noneCategory).distinctBy { if (it.id == 0L) "none" else it.id.toString() }
}
when (selectedTab) {
@@ -131,6 +139,8 @@ fun ProjectStatisticsScreen(
2 -> {
ProjectSankeyDiagram(
projectName = proj.name.ifEmpty { proj.remoteId },
projectId = proj.id,
projectRemoteId = proj.remoteId,
allMembers = data.members,
allBills = data.bills,
customCategories = sankeyCategories,
@@ -44,8 +44,8 @@ fun ProjectStatisticsTable(
val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) }
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
var categoryId by remember { mutableIntStateOf(-1000) }
var paymentModeId by remember { mutableIntStateOf(-1000) }
var categoryId by remember { mutableLongStateOf(-1000L) }
var paymentModeId by remember { mutableLongStateOf(-1000L) }
var dateMin by remember { mutableStateOf<String?>(null) }
var dateMax by remember { mutableStateOf<String?>(null) }
@@ -57,14 +57,14 @@ fun ProjectStatisticsTable(
val paymentModeAll = stringResource(R.string.payment_mode_all)
val paymentModeNone = stringResource(R.string.payment_mode_none)
val shareStatsHeader = stringResource(R.string.share_stats_header)
val shareStatsIntro = stringResource(R.string.share_stats_intro, proj.name.ifEmpty { proj.remoteId })
val shareStatsHeader = stringResource(R.string.msg_stats_header)
val shareStatsIntro = stringResource(R.string.msg_stats_intro, proj.name.ifEmpty { proj.remoteId })
val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) {
val list = mutableListOf<Triple<Int, String, String>>()
list.add(Triple(-1000, "📋", categoryAll))
list.add(Triple(-100, "🧾", categoryAllExceptReimbursement))
list.add(Triple(0, "", categoryNone))
val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000L, "📋", categoryAll))
list.add(Triple(-100L, "🧾", categoryAllExceptReimbursement))
list.add(Triple(0L, "", categoryNone))
val catsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultCategories(context, proj.id)
@@ -75,15 +75,15 @@ fun ProjectStatisticsTable(
}
catsToUse.forEach {
list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: ""))
list.add(Triple(it.id, it.icon, it.name ?: ""))
}
list.distinctBy { it.first }
}
val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) {
val list = mutableListOf<Triple<Int, String, String>>()
list.add(Triple(-1000, "💳", paymentModeAll))
list.add(Triple(0, "", paymentModeNone))
val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000L, "💳", paymentModeAll))
list.add(Triple(0L, "", paymentModeNone))
val pmsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultPaymentModes(context, proj.id)
@@ -94,7 +94,7 @@ fun ProjectStatisticsTable(
}
pmsToUse.forEach {
list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: ""))
list.add(Triple(it.id, it.icon, it.name ?: ""))
}
list.distinctBy { it.first }
}
@@ -105,10 +105,12 @@ fun ProjectStatisticsTable(
val membersPaid = HashMap<Long, Double>()
val membersSpent = HashMap<Long, Double>()
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(customCategories, proj.id, proj.remoteId)
SupportUtil.getStats(
allMembers, allBills,
membersNbBills, membersBalance, membersPaid, membersSpent,
categoryId, paymentModeId, dateMin, dateMax
categoryId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
)
var statsText = shareStatsIntro + "\n\n"
@@ -164,12 +166,12 @@ fun ProjectStatisticsTable(
EditableExposedDropdownMenu(
value = selectedCategory?.third ?: "",
placeholder = stringResource(R.string.setting_category),
placeholder = stringResource(R.string.label_category),
expanded = categoryExpanded,
onExpandedChange = { categoryExpanded = it },
onDismissRequest = { categoryExpanded = false },
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
Box(modifier = Modifier) {
if (selectedCategory != null) {
Text(text = selectedCategory.second, fontSize = 20.sp)
} else {
@@ -198,12 +200,12 @@ fun ProjectStatisticsTable(
EditableExposedDropdownMenu(
value = selectedPm?.third ?: "",
placeholder = stringResource(R.string.setting_payment_mode),
placeholder = stringResource(R.string.label_mode),
expanded = pmExpanded,
onExpandedChange = { pmExpanded = it },
onDismissRequest = { pmExpanded = false },
leadingIcon = {
Box(modifier = Modifier.padding(start = 12.dp)) {
Box(modifier = Modifier) {
if (selectedPm != null) {
Text(text = selectedPm.second, fontSize = 20.sp)
} else {
@@ -269,17 +271,15 @@ fun ProjectStatisticsTable(
Spacer(modifier = Modifier.height(24.dp))
// Table Header
Surface(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.05f),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxWidth()
) {
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp)) {
Text(stringResource(R.string.stats_who), modifier = Modifier.weight(2f), fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
Text(stringResource(R.string.stats_paid), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
Text(stringResource(R.string.stats_spent), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
Text(stringResource(R.string.stats_balance), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 12.dp)) {
Text(stringResource(R.string.stats_who).uppercase(), modifier = Modifier.weight(2f), fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
Text(stringResource(R.string.stats_paid).uppercase(), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
Text(stringResource(R.string.stats_spent).uppercase(), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
Text(stringResource(R.string.stats_balance).uppercase(), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
}
}
@@ -12,6 +12,6 @@ class DBAccountProject(
) : Serializable {
override fun toString(): String {
return "#DBAccountProject$id/$remoteId,$name, $ncUrl, $password, archivedTs=$archivedTs"
return "#DBAccountProject$id/$remoteId,$name, $ncUrl, [SECURE], archivedTs=$archivedTs"
}
}
@@ -1,6 +1,5 @@
package net.helcel.cowspent.model
import android.util.Log
import java.io.Serializable
import java.util.Calendar
import java.util.Locale
@@ -16,9 +15,9 @@ open class DBBill(
var state: Int,
var repeat: String?,
var paymentMode: String?,
var categoryRemoteId: Int,
var categoryId: Long,
var comment: String?,
var paymentModeRemoteId: Int
var paymentModeId: Long
) : Item, Serializable {
var formattedWhat: String = ""
@@ -39,7 +38,6 @@ open class DBBill(
get() {
val cal = Calendar.getInstance()
cal.timeInMillis = timestamp * 1000
Log.v("ll", "[$what] get date ts $timestamp year ${cal[Calendar.YEAR]}")
val month = cal[Calendar.MONTH] + 1
val day = cal[Calendar.DAY_OF_MONTH]
return "${cal[Calendar.YEAR]}-${String.format(Locale.ROOT, "%02d", month)}-${
@@ -58,7 +56,7 @@ open class DBBill(
override fun toString(): String {
return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryRemoteId"
return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryId"
}
override fun isSection(): Boolean {
@@ -73,15 +71,15 @@ open class DBBill(
const val PAYMODE_TRANSFER = "t"
const val PAYMODE_ONLINE_SERVICE = "o"
const val PAYMODE_ID_NONE = 0
const val PAYMODE_ID_CARD = -1
const val PAYMODE_ID_CASH = -2
const val PAYMODE_ID_CHECK = -3
const val PAYMODE_ID_TRANSFER = -4
const val PAYMODE_ID_ONLINE_SERVICE = -5
const val PAYMODE_ID_NONE = 0L
const val PAYMODE_ID_CARD = -1L
const val PAYMODE_ID_CASH = -2L
const val PAYMODE_ID_CHECK = -3L
const val PAYMODE_ID_TRANSFER = -4L
const val PAYMODE_ID_ONLINE_SERVICE = -5L
@JvmField
val oldPmIdToNew: Map<String, Int> = object : HashMap<String, Int>() {
val oldPmIdToNew: Map<String, Long> = object : HashMap<String, Long>() {
init {
put(PAYMODE_NONE, PAYMODE_ID_NONE)
put(PAYMODE_CARD, PAYMODE_ID_CARD)
@@ -92,19 +90,19 @@ open class DBBill(
}
}
const val CATEGORY_NONE = 0
const val CATEGORY_GROCERIES = -1
const val CATEGORY_LEISURE = -2
const val CATEGORY_RENT = -3
const val CATEGORY_BILLS = -4
const val CATEGORY_CULTURE = -5
const val CATEGORY_HEALTH = -6
const val CATEGORY_SHOPPING = -10
const val CATEGORY_REIMBURSEMENT = -11
const val CATEGORY_RESTAURANT = -12
const val CATEGORY_ACCOMMODATION = -13
const val CATEGORY_TRANSPORT = -14
const val CATEGORY_SPORT = -15
const val CATEGORY_NONE = 0L
const val CATEGORY_GROCERIES = -1L
const val CATEGORY_LEISURE = -2L
const val CATEGORY_RENT = -3L
const val CATEGORY_BILLS = -4L
const val CATEGORY_CULTURE = -5L
const val CATEGORY_HEALTH = -6L
const val CATEGORY_SHOPPING = -10L
const val CATEGORY_REIMBURSEMENT = -11L
const val CATEGORY_RESTAURANT = -12L
const val CATEGORY_ACCOMMODATION = -13L
const val CATEGORY_TRANSPORT = -14L
const val CATEGORY_SPORT = -15L
const val STATE_OK = 0
const val STATE_ADDED = 1
@@ -8,7 +8,8 @@ class DBCategory(
var projectId: Long,
var name: String?,
var icon: String,
var color: String
var color: String,
var state: Int = DBBill.STATE_OK
) : Serializable {
override fun toString(): String {
@@ -8,7 +8,8 @@ class DBPaymentMode(
var projectId: Long,
var name: String?,
var icon: String,
var color: String
var color: String,
var state: Int = DBBill.STATE_OK
) : Serializable {
override fun toString(): String {
@@ -15,9 +15,9 @@ class GroupedBill(
sourceBills.first().state,
sourceBills.first().repeat,
sourceBills.first().paymentMode,
sourceBills.first().categoryRemoteId,
sourceBills.first().categoryId,
sourceBills.first().comment,
sourceBills.first().paymentModeRemoteId
sourceBills.first().paymentModeId
), Serializable {
init {
this.formattedWhat = sourceBills.first().formattedWhat
File diff suppressed because it is too large Load Diff
@@ -265,6 +265,121 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val members = dbHelper.getMembersOfProject(project.id, null)
val memberIdToRemoteId = members.associate { it.id to it.remoteId }
val categoriesToAdd = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (catToAdd in categoriesToAdd) {
try {
val categoriesResponse = client!!.getCategories(project)
val remoteCategories = categoriesResponse.getCategories(project.id)
val matchingRemote = remoteCategories.find { it.name == catToAdd.name }
if (matchingRemote != null) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, matchingRemote.remoteId)
catToAdd.remoteId = matchingRemote.remoteId
} else {
val createResponse = client!!.createRemoteCategory(project, catToAdd)
val newRemoteId = createResponse.remoteCategoryId
if (newRemoteId > 0) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
catToAdd.remoteId = newRemoteId
}
}
} catch (e: Exception) {
Log.e(TAG, "CATEGORY SYNC FAILED for ${catToAdd.name}", e)
}
}
val categoriesToEdit = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (catToEdit in categoriesToEdit) {
try {
client!!.editRemoteCategory(project, catToEdit)
dbHelper.updateCategory(catToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT CATEGORY FAILED for ${catToEdit.name}, might not exist remotely", e)
// If it fails, we keep the state as EDITED so it tries again next time,
// or we could set it to OK if we think it's a permanent mismatch.
// For now just log it.
}
}
val categoriesToDelete = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (catToDel in categoriesToDelete) {
try {
client!!.deleteRemoteCategory(project, catToDel.remoteId)
dbHelper.deleteCategory(catToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete category on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deleteCategory(catToDel.id)
} else {
throw e
}
} catch (e: Exception) {
Log.e(TAG, "DELETE CATEGORY FAILED for ${catToDel.name}", e)
}
}
val paymentModesToAdd = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_ADDED)
if (paymentModesToAdd.isNotEmpty()) {
try {
val pmsResponse = client!!.getPaymentModes(project)
val remotePms = pmsResponse.getPaymentModes(project.id)
val remotePmsNames = remotePms.map { it.name }
for (pmToAdd in paymentModesToAdd) {
val searchIndex = remotePmsNames.indexOf(pmToAdd.name)
if (searchIndex != -1) {
val remotePm = remotePms[searchIndex]
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, remotePm.remoteId)
pmToAdd.remoteId = remotePm.remoteId
} else {
val createRemotePaymentModeResponse = client!!.createRemotePaymentMode(project, pmToAdd)
val newRemoteId = createRemotePaymentModeResponse.remotePaymentModeId
if (newRemoteId > 0) {
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
pmToAdd.remoteId = newRemoteId
}
}
}
} catch (e: NextcloudHttpRequestFailedException) {
Log.e(TAG, "GET PAYMENT MODES FAILED : " + e.message)
}
}
val paymentModesToEdit = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (pmToEdit in paymentModesToEdit) {
try {
client!!.editRemotePaymentMode(project, pmToEdit)
dbHelper.updatePaymentMode(pmToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT PAYMENT MODE FAILED for ${pmToEdit.name}, might not exist remotely", e)
}
}
val paymentModesToDelete = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (pmToDel in paymentModesToDelete) {
try {
client!!.deleteRemotePaymentMode(project, pmToDel.remoteId)
dbHelper.deletePaymentMode(pmToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete payment mode on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deletePaymentMode(pmToDel.id)
} else {
throw e
}
} catch (_: Exception) {
}
}
val categories = dbHelper.getCategories(project.id)
val categoryIdToRemoteId = categories.associate { it.id to it.remoteId }.toMutableMap()
// Map hardcoded constants to themselves if not in DB
categoryIdToRemoteId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
categories.filter { it.remoteId < 0 }.forEach { categoryIdToRemoteId[it.remoteId] = it.remoteId }
val paymentModes = dbHelper.getPaymentModes(project.id)
val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId }.toMutableMap()
paymentModes.filter { it.remoteId < 0 }.forEach { paymentModeIdToRemoteId[it.remoteId] = it.remoteId }
val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (bToDel in toDelete) {
try {
@@ -293,12 +408,16 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toEdit = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (bToEdit in toEdit) {
try {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId)
if (editRemoteBillResponse.stringContent == bToEdit.remoteId.toString()) {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val returnedRemoteId = editRemoteBillResponse.remoteBillId
if (returnedRemoteId == bToEdit.remoteId || (returnedRemoteId == 0L && !project.getRequestBaseUrl(true).contains("/ocs/v2.php"))) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition (${editRemoteBillResponse.stringContent})")
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else if (returnedRemoteId > 0) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else {
Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})")
Log.d(TAG, "FAILED to edit remote bill ($returnedRemoteId)")
}
} catch (_: Exception) {
Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely")
@@ -307,8 +426,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (bToAdd in toAdd) {
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId)
val newRemoteId = createRemoteBillResponse.stringContent.toLong()
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val newRemoteId = createRemoteBillResponse.remoteBillId
if (newRemoteId > 0) {
dbHelper.updateBill(
bToAdd.id, newRemoteId, null,
@@ -320,48 +439,69 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
}
}
val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (cToDel in currenciesToDelete) {
try {
val deleteRemoteCurrencyResponse = client!!.deleteRemoteCurrency(project, cToDel.remoteId)
if (deleteRemoteCurrencyResponse.stringContent == "OK") {
Log.d(TAG, "successfully deleted currency on remote project : delete it locally")
dbHelper.deleteCurrency(cToDel.id)
}
} catch (e: IOException) {
if (e.message == "\"Not Found\"") {
Log.d(TAG, "failed to delete currency on remote project : delete it locally anyway")
dbHelper.deleteCurrency(cToDel.id)
} else {
throw e
if (project.type == ProjectType.COSPEND) {
val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (cToDel in currenciesToDelete) {
try {
val deleteRemoteCurrencyResponse = client!!.deleteRemoteCurrency(project, cToDel.remoteId)
if (deleteRemoteCurrencyResponse.stringContent == "OK") {
Log.d(TAG, "successfully deleted currency on remote project : delete it locally")
dbHelper.deleteCurrency(cToDel.id)
}
} catch (e: IOException) {
if (e.message == "\"Not Found\"") {
Log.d(TAG, "failed to delete currency on remote project : delete it locally anyway")
dbHelper.deleteCurrency(cToDel.id)
} else {
throw e
}
}
}
} else {
val currenciesToDelete = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (cToDel in currenciesToDelete) {
dbHelper.deleteCurrency(cToDel.id)
}
}
val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (cToEdit in currenciesToEdit) {
try {
val editRemoteCurrencyResponse = client!!.editRemoteCurrency(project, cToEdit)
if (editRemoteCurrencyResponse.stringContent == cToEdit.remoteId.toString()) {
dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote currency edition (${editRemoteCurrencyResponse.stringContent})")
} else {
Log.d(TAG, "FAILED to edit remote currency (${editRemoteCurrencyResponse.stringContent})")
}
} catch (e: IOException) {
if (e.message == "{\"message\": \"Internal Server Error\"}") {
Log.d(TAG, "FAILED to edit remote currency : it does not exist remotely")
} else {
throw e
if (project.type == ProjectType.COSPEND) {
val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (cToEdit in currenciesToEdit) {
try {
val editRemoteCurrencyResponse = client!!.editRemoteCurrency(project, cToEdit)
if (editRemoteCurrencyResponse.stringContent == cToEdit.remoteId.toString()) {
dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote currency edition (${editRemoteCurrencyResponse.stringContent})")
} else {
Log.d(TAG, "FAILED to edit remote currency (${editRemoteCurrencyResponse.stringContent})")
}
} catch (e: IOException) {
if (e.message == "{\"message\": \"Internal Server Error\"}") {
Log.d(TAG, "FAILED to edit remote currency : it does not exist remotely")
} else {
throw e
}
}
}
} else {
val currenciesToEdit = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (cToEdit in currenciesToEdit) {
dbHelper.setCurrencyState(cToEdit.id, DBBill.STATE_OK)
}
}
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) {
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong()
if (newRemoteId > 0) {
if (project.type == ProjectType.COSPEND) {
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) {
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
val newRemoteId = createRemoteCurrencyResponse.remoteCurrencyId
if (newRemoteId > 0) {
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
}
}
} else {
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) {
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
}
}
@@ -450,7 +590,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localPaymentModes = dbHelper.getPaymentModes(project.id)
for (localPaymentMode in localPaymentModes) {
if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
if (localPaymentMode.state == DBBill.STATE_OK && !remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
dbHelper.deletePaymentMode(localPaymentMode.id)
Log.d(TAG, "Delete local pm : $localPaymentMode")
}
@@ -460,6 +600,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId }
for (c in remoteCategories) {
if (c.remoteId == DBBill.CATEGORY_REIMBURSEMENT) continue
val localCategory = dbHelper.getCategory(c.remoteId, project.id)
if (localCategory == null) {
Log.d(TAG, "Add local category : $c")
@@ -479,7 +620,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCategories = dbHelper.getCategories(project.id)
for (localCategory in localCategories) {
if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
if (localCategory.state == DBBill.STATE_OK && !remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
dbHelper.deleteCategory(localCategory.id)
Log.d(TAG, "Delete local category : $localCategory")
}
@@ -507,7 +648,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCurrencies = dbHelper.getCurrencies(project.id)
for (localCurrency in localCurrencies) {
if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
if (localCurrency.state == DBBill.STATE_OK && !remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
dbHelper.deleteCurrency(localCurrency.id)
Log.d(TAG, "Delete local currency : $localCurrencies")
}
@@ -574,13 +715,23 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val dbMembers = dbHelper.getMembersOfProject(project.id, null)
val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id }
val dbCategories = dbHelper.getCategories(project.id)
val categoriesRemoteIdToId = dbCategories.associate { it.remoteId to it.id }.toMutableMap()
// Map hardcoded constants to their local IDs if they exist in DB, else to themselves
categoriesRemoteIdToId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
dbCategories.filter { it.remoteId < 0 }.forEach { categoriesRemoteIdToId[it.remoteId] = it.id }
val dbPaymentModes = dbHelper.getPaymentModes(project.id)
val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap()
dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id }
val billsResponse = client!!.getBills(project)
val isIHM = project.type == ProjectType.IHATEMONEY
val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
val remoteBills: List<DBBill> = if (isIHM) {
billsResponse.getBillsIHM(project.id, memberRemoteIdToId)
billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
} else {
billsResponse.getBillsCospend(project.id, memberRemoteIdToId)
billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
}
val remoteAllBillIds: List<Long> = if (isIHM) {
remoteBills.map { it.remoteId }
@@ -597,7 +748,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
dbHelper.addBill(remoteBill)
nbPulledNewBills++
newBillsDialogText += "+ ${remoteBill.what}\n"
Log.d(TAG, "Add local bill : $remoteBill")
} else {
val localBill = localBillsByRemoteId[remoteBill.remoteId]!!
if (hasChanged(localBill, remoteBill)) {
@@ -605,12 +755,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
localBill.id, null, remoteBill.payerId,
remoteBill.amount, remoteBill.timestamp,
remoteBill.what, DBBill.STATE_OK, remoteBill.repeat,
remoteBill.paymentMode, remoteBill.paymentModeRemoteId,
remoteBill.categoryRemoteId, remoteBill.comment
remoteBill.paymentMode, remoteBill.paymentModeId,
remoteBill.categoryId, remoteBill.comment
)
nbPulledUpdatedBills++
updatedBillsDialogText += "${remoteBill.what}\n"
Log.d(TAG, "Update local bill : $remoteBill")
} else {
Log.d(TAG, "Nothing to do for bill : $localBill")
}
@@ -1063,8 +1212,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
localBill.what == remoteBill.what &&
localBill.comment == remoteBill.comment &&
localBill.paymentMode == remoteBill.paymentMode &&
localBill.paymentModeRemoteId == remoteBill.paymentModeRemoteId &&
localBill.categoryRemoteId == remoteBill.categoryRemoteId
localBill.paymentModeId == remoteBill.paymentModeId &&
localBill.categoryId == remoteBill.categoryId
) {
val localRepeat = localBill.repeat ?: DBBill.NON_REPEATED
val remoteRepeat = remoteBill.repeat ?: DBBill.NON_REPEATED
@@ -111,6 +111,7 @@ object ThemeUtils {
modifier = Modifier
.weight(1f)
.navigationBarsPadding()
.imePadding()
) {
content()
}
@@ -14,11 +14,11 @@ object BillFormatter {
) {
for (bill in bills) {
var whatPrefix = ""
val pm = paymentModesMap[bill.paymentModeRemoteId.toLong()]
val pm = paymentModesMap[bill.paymentModeId]
if (pm != null) {
whatPrefix += pm.icon + " "
} else {
when (bill.paymentModeRemoteId) {
when (bill.paymentModeId) {
DBBill.PAYMODE_ID_CARD -> whatPrefix += "\uD83D\uDCB3 "
DBBill.PAYMODE_ID_CASH -> whatPrefix += "\uD83D\uDCB5 "
DBBill.PAYMODE_ID_CHECK -> whatPrefix += "\uD83C\uDFAB "
@@ -27,11 +27,11 @@ object BillFormatter {
}
}
val cat = categoriesMap[bill.categoryRemoteId.toLong()]
val cat = categoriesMap[bill.categoryId]
if (cat != null) {
whatPrefix += cat.icon + " "
} else {
when (bill.categoryRemoteId) {
when (bill.categoryId) {
DBBill.CATEGORY_GROCERIES -> whatPrefix += "\uD83D\uDED2 "
DBBill.CATEGORY_LEISURE -> whatPrefix += "\uD83C\uDF89 "
DBBill.CATEGORY_RENT -> whatPrefix += "\uD83C\uDFE0 "
@@ -10,28 +10,57 @@ object CategoryUtils {
fun getDefaultCategories(context: Context, projectId: Long): List<DBCategory> {
return listOf(
DBCategory(0, DBBill.CATEGORY_GROCERIES.toLong(), projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"),
DBCategory(0, DBBill.CATEGORY_LEISURE.toLong(), projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"),
DBCategory(0, DBBill.CATEGORY_RENT.toLong(), projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"),
DBCategory(0, DBBill.CATEGORY_BILLS.toLong(), projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"),
DBCategory(0, DBBill.CATEGORY_CULTURE.toLong(), projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"),
DBCategory(0, DBBill.CATEGORY_HEALTH.toLong(), projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"),
DBCategory(0, DBBill.CATEGORY_SHOPPING.toLong(), projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"),
DBCategory(0, DBBill.CATEGORY_REIMBURSEMENT.toLong(), projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"),
DBCategory(0, DBBill.CATEGORY_RESTAURANT.toLong(), projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"),
DBCategory(0, DBBill.CATEGORY_ACCOMMODATION.toLong(), projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"),
DBCategory(0, DBBill.CATEGORY_TRANSPORT.toLong(), projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"),
DBCategory(0, DBBill.CATEGORY_SPORT.toLong(), projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177")
DBCategory(DBBill.CATEGORY_GROCERIES,
DBBill.CATEGORY_GROCERIES, projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"),
DBCategory(DBBill.CATEGORY_LEISURE,
DBBill.CATEGORY_LEISURE, projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"),
DBCategory(DBBill.CATEGORY_RENT,
DBBill.CATEGORY_RENT, projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"),
DBCategory(DBBill.CATEGORY_BILLS,
DBBill.CATEGORY_BILLS, projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"),
DBCategory(DBBill.CATEGORY_CULTURE,
DBBill.CATEGORY_CULTURE, projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"),
DBCategory(DBBill.CATEGORY_HEALTH,
DBBill.CATEGORY_HEALTH, projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"),
DBCategory(DBBill.CATEGORY_SHOPPING,
DBBill.CATEGORY_SHOPPING, projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"),
DBCategory(DBBill.CATEGORY_REIMBURSEMENT,
DBBill.CATEGORY_REIMBURSEMENT, projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"),
DBCategory(DBBill.CATEGORY_RESTAURANT,
DBBill.CATEGORY_RESTAURANT, projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"),
DBCategory(DBBill.CATEGORY_ACCOMMODATION,
DBBill.CATEGORY_ACCOMMODATION, projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"),
DBCategory(DBBill.CATEGORY_TRANSPORT,
DBBill.CATEGORY_TRANSPORT, projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"),
DBCategory(DBBill.CATEGORY_SPORT,
DBBill.CATEGORY_SPORT, projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177")
)
}
fun getDefaultPaymentModes(context: Context, projectId: Long): List<DBPaymentMode> {
return listOf(
DBPaymentMode(0, DBBill.PAYMODE_ID_CARD.toLong(), projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"),
DBPaymentMode(0, DBBill.PAYMODE_ID_CASH.toLong(), projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"),
DBPaymentMode(0, DBBill.PAYMODE_ID_CHECK.toLong(), projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"),
DBPaymentMode(0, DBBill.PAYMODE_ID_TRANSFER.toLong(), projectId, context.getString(R.string.payment_mode_transfer), "", "#00ced1"),
DBPaymentMode(0, DBBill.PAYMODE_ID_ONLINE_SERVICE.toLong(), projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc")
DBPaymentMode(DBBill.PAYMODE_ID_CARD, DBBill.PAYMODE_ID_CARD, projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"),
DBPaymentMode(DBBill.PAYMODE_ID_CASH, DBBill.PAYMODE_ID_CASH, projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"),
DBPaymentMode(DBBill.PAYMODE_ID_CHECK, DBBill.PAYMODE_ID_CHECK, projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"),
DBPaymentMode(DBBill.PAYMODE_ID_TRANSFER, DBBill.PAYMODE_ID_TRANSFER, projectId, context.getString(R.string.payment_mode_transfer), "", "#00ced1"),
DBPaymentMode(DBBill.PAYMODE_ID_ONLINE_SERVICE, DBBill.PAYMODE_ID_ONLINE_SERVICE, projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc")
)
}
fun getReimbursementCategoryId(categories: List<DBCategory>, projectId: Long, projectRemoteId: String? = null): Long {
return categories.find {
it.projectId == projectId && (
it.remoteId == DBBill.CATEGORY_REIMBURSEMENT ||
(projectRemoteId != null && it.remoteId.toString() == projectRemoteId)
)
}?.id ?: DBBill.CATEGORY_REIMBURSEMENT
}
fun getCategoryById(context: Context, id: Long, projectId: Long = 0): DBCategory? {
return getDefaultCategories(context, projectId).find { it.id == id }
}
fun getPaymentModeById(context: Context, id: Long, projectId: Long = 0): DBPaymentMode? {
return getDefaultPaymentModes(context, projectId).find { it.id == id }
}
}
@@ -17,7 +17,7 @@ object CospendClientUtil {
enum class LoginStatus(@param:StringRes val str: Int) {
OK(0),
AUTH_FAILED(R.string.error_username_password_invalid),
AUTH_FAILED(R.string.error_auth),
CONNECTION_FAILED(R.string.error_io),
NO_NETWORK(R.string.error_no_network),
JSON_FAILED(R.string.error_json),
@@ -48,7 +48,7 @@ object ExportUtil {
}
owersTxt = owersTxt.replace(",$".toRegex(), "")
fileContent += "\"${b.what}\",${b.amount},${b.date},${b.timestamp},\"$payerName\"," +
"$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryRemoteId}," +
"$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryId}," +
"${b.paymentMode}\n"
}
@@ -35,13 +35,13 @@ class NextcloudClient(
val method = if (useOcsApi) METHOD_GET else METHOD_POST
return if (nextcloudAPI != null) {
Log.d(javaClass.simpleName, "using SSO to get/sync account projects")
Log.d(javaClass.simpleName, "Sync projects target $target")
// Log.d(javaClass.simpleName, "Sync projects target $target")
ServerResponse.AccountProjectsResponse(
requestServerWithSSO(nextcloudAPI, target, method, null, useOcsApi),
useOcsApi
)
} else {
Log.d(javaClass.simpleName, "Sync projects target $target")
// Log.d(javaClass.simpleName, "Sync projects target $target")
ServerResponse.AccountProjectsResponse(
requestServer(target, method, null, "", true, useOcsApi),
useOcsApi
@@ -93,6 +93,10 @@ class NextcloudClient(
params: Collection<QueryParam>?,
isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val headers: MutableMap<String, List<String>> = HashMap()
if (isOCSRequest) {
@@ -103,13 +107,13 @@ class NextcloudClient(
val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setHeader(headers)
.build()
} else {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setParameter(params)
.setHeader(headers)
.build()
@@ -182,9 +186,13 @@ class NextcloudClient(
target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val targetURL = url + target.replace("^/".toRegex(), "")
Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
val targetURL = url + finalTarget.replace("^/".toRegex(), "")
// Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
val httpCon = SupportUtil.getHttpURLConnection(targetURL)
httpCon.requestMethod = method
if (needLogin) {
@@ -194,7 +202,7 @@ class NextcloudClient(
)
}
httpCon.setRequestProperty("Connection", "Close")
httpCon.setRequestProperty("User-Agent", "cowspent-android/" + SupportUtil.getAppVersionName(context))
httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context))
if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag)
}
@@ -206,7 +214,7 @@ class NextcloudClient(
var paramData: ByteArray? = null
if (params != null) {
paramData = params.toString().toByteArray()
Log.d(javaClass.simpleName, "Params: $params")
// Log.d(javaClass.simpleName, "Params: $params")
httpCon.setFixedLengthStreamingMode(paramData.size)
httpCon.setRequestProperty("Content-Type", application_json)
httpCon.doOutput = true
@@ -244,8 +252,12 @@ class NextcloudClient(
target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
var strBase64: String
val targetURL = url + target.replace("^/".toRegex(), "")
val targetURL = url + finalTarget.replace("^/".toRegex(), "")
val httpCon = SupportUtil.getHttpURLConnection( targetURL)
httpCon.requestMethod = method
if (needLogin) {
@@ -263,11 +275,11 @@ class NextcloudClient(
httpCon.setRequestProperty("OCS-APIRequest", "true")
}
httpCon.connectTimeout = 10 * 1000 // 10 seconds
Log.d(javaClass.simpleName, "$method $targetURL")
// Log.d(javaClass.simpleName, "$method $targetURL")
var paramData: ByteArray?
if (params != null) {
paramData = params.toString().toByteArray()
Log.d(javaClass.simpleName, "Params: $params")
// Log.d(javaClass.simpleName, "Params: $params")
httpCon.setFixedLengthStreamingMode(paramData.size)
httpCon.setRequestProperty("Content-Type", application_json)
httpCon.doOutput = true
@@ -0,0 +1,84 @@
package net.helcel.cowspent.util
import android.content.Context
import android.util.Base64
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.RegistryConfiguration
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.integration.android.AndroidKeysetManager
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "secure_prefs")
object SecureStorage {
private var aead: Aead? = null
@Synchronized
private fun getAead(context: Context): Aead {
aead?.let { return it }
AeadConfig.register()
val masterKeyUri = "android-keystore://secure_storage_master_key"
val keysetManager = AndroidKeysetManager.Builder()
.withSharedPref(context, "tink_keyset", "secure_storage_keyset_prefs")
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withMasterKeyUri(masterKeyUri)
.build()
val newAead = keysetManager.keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
aead = newAead
return newAead
}
private fun encrypt(context: Context, value: String): String {
val encrypted = getAead(context).encrypt(value.toByteArray(), null)
return Base64.encodeToString(encrypted, Base64.NO_WRAP)
}
private fun decrypt(context: Context, encryptedValue: String): String? {
return try {
val decoded = Base64.decode(encryptedValue, Base64.NO_WRAP)
val decrypted = getAead(context).decrypt(decoded, null)
String(decrypted)
} catch (_: Exception) {
null
}
}
suspend fun savePassword(context: Context, key: String, password: String?) {
if (password == null) {
removePassword(context, key)
} else {
val encrypted = encrypt(context, password)
context.dataStore.edit { it[stringPreferencesKey(key)] = encrypted }
}
}
suspend fun getPassword(context: Context, key: String): String? {
val encrypted = context.dataStore.data.map { it[stringPreferencesKey(key)] }.first()
return encrypted?.let { decrypt(context, it) }
}
suspend fun removePassword(context: Context, key: String) {
context.dataStore.edit { it.remove(stringPreferencesKey(key)) }
}
// Synchronous alternatives for legacy code
fun savePasswordSync(context: Context, key: String, password: String?) = runBlocking {
savePassword(context, key, password)
}
fun getPasswordSync(context: Context, key: String): String? = runBlocking {
getPassword(context, key)
}
fun removePasswordSync(context: Context, key: String) = runBlocking {
removePassword(context, key)
}
}
@@ -137,6 +137,90 @@ open class ServerResponse(
getResponseStringData().toLong()
}
class CreateRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteCategoryId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class DeleteRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class CreateRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remotePaymentModeId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class DeleteRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class CreateRemoteCurrencyResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
@@ -145,6 +229,18 @@ open class ServerResponse(
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteCurrencyId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemoteCurrencyResponse(
@@ -196,6 +292,18 @@ open class ServerResponse(
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteBillId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class CreateRemoteBillResponse(
@@ -206,6 +314,18 @@ open class ServerResponse(
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteBillId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class DeleteRemoteBillResponse(
@@ -242,13 +362,23 @@ open class ServerResponse(
ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class)
fun getBillsCospend(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> {
return getBillsFromJSONObject(getResponseObjectData(), projId, memberRemoteIdToId)
fun getBillsCospend(
projId: Long,
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
return getBillsFromJSONObject(projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
}
@Throws(JSONException::class)
fun getBillsIHM(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> {
return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId)
fun getBillsIHM(
projId: Long,
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
}
@@ -270,6 +400,32 @@ open class ServerResponse(
}
}
class CategoriesResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) :
ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class)
fun getCategories(projId: Long): List<DBCategory> {
return if (isOcsResponse) {
getCategoriesFromJSON(getResponseObjectData(), projId)
} else {
getCategoriesFromJSONArray(getResponseArrayData(), projId)
}
}
}
class PaymentModesResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) :
ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class)
fun getPaymentModes(projId: Long): List<DBPaymentMode> {
return if (isOcsResponse) {
getPaymentModesFromJSON(getResponseObjectData(), projId)
} else {
getPaymentModesFromJSONArray(getResponseArrayData(), projId)
}
}
}
class AccountProjectsResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
@@ -366,6 +522,26 @@ open class ServerResponse(
return members
}
@Throws(JSONException::class)
protected fun getCategoriesFromJSONArray(jsonCats: JSONArray, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = ArrayList()
for (i in 0 until jsonCats.length()) {
val jsonCat = jsonCats.getJSONObject(i)
categories.add(getCategoryFromJSON(jsonCat, projId))
}
return categories
}
@Throws(JSONException::class)
protected fun getPaymentModesFromJSONArray(jsonPms: JSONArray, projId: Long): List<DBPaymentMode> {
val paymentModes: MutableList<DBPaymentMode> = ArrayList()
for (i in 0 until jsonPms.length()) {
val jsonPm = jsonPms.getJSONObject(i)
paymentModes.add(getPaymentModeFromJSON(jsonPm, projId))
}
return paymentModes
}
@Throws(JSONException::class)
protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = ArrayList()
@@ -382,6 +558,27 @@ open class ServerResponse(
return categories
}
@Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, projId: Long): DBCategory {
var remoteId: Long = 0
if (json.has("id") && !json.isNull("id")) {
remoteId = json.getLong("id")
}
var name = ""
var color = ""
var icon = ""
if (json.has("color") && !json.isNull("color")) {
color = json.getString("color")
}
if (json.has("icon") && !json.isNull("icon")) {
icon = json.getString("icon")
}
if (json.has("name") && !json.isNull("name")) {
name = json.getString("name")
}
return DBCategory(0, remoteId, projId, name, icon, color)
}
@Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory {
val remoteId = remoteIdStr.toLong()
@@ -416,6 +613,27 @@ open class ServerResponse(
return paymentModes
}
@Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, projId: Long): DBPaymentMode {
var remoteId: Long = 0
if (json.has("id") && !json.isNull("id")) {
remoteId = json.getLong("id")
}
var name = ""
var color = ""
var icon = ""
if (json.has("color") && !json.isNull("color")) {
color = json.getString("color")
}
if (json.has("icon") && !json.isNull("icon")) {
icon = json.getString("icon")
}
if (json.has("name") && !json.isNull("name")) {
name = json.getString("name")
}
return DBPaymentMode(0, remoteId, projId, name, icon, color)
}
@Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode {
val remoteId = remoteIdStr.toLong()
@@ -556,26 +774,30 @@ open class ServerResponse(
protected fun getBillsFromJSONArray(
json: JSONArray,
projId: Long,
memberRemoteIdToId: Map<Long, Long>
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
val bills: MutableList<DBBill> = ArrayList()
for (i in 0 until json.length()) {
val jsonBill = json.getJSONObject(i)
bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId))
bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId))
}
return bills
}
@Throws(JSONException::class)
protected fun getBillsFromJSONObject(
json: JSONObject,
projId: Long,
memberRemoteIdToId: Map<Long, Long>
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
val bills: List<DBBill>
val json = getResponseObjectData()
if (json.has("bills") && !json.isNull("bills")) {
val jsonBills = json.getJSONArray("bills")
bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId)
bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} else {
bills = ArrayList()
}
@@ -586,7 +808,9 @@ open class ServerResponse(
protected fun getBillFromJSON(
json: JSONObject,
projId: Long,
memberRemoteIdToId: Map<Long, Long>
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): DBBill {
var remoteId: Long = 0
var payerRemoteId: Long
@@ -600,7 +824,7 @@ open class ServerResponse(
var repeat = DBBill.NON_REPEATED
var paymentMode = DBBill.PAYMODE_NONE
var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE
var categoryId = DBBill.CATEGORY_NONE
var categoryRemoteId = DBBill.CATEGORY_NONE
if (!json.isNull("id")) {
remoteId = json.getLong("id")
}
@@ -639,23 +863,29 @@ open class ServerResponse(
}
if (json.has("paymentmode") && !json.isNull("paymentmode")) {
paymentMode = json.getString("paymentmode")
} else if (json.has("paymentMode") && !json.isNull("paymentMode")) {
paymentMode = json.getString("paymentMode")
}
if (json.has("categoryid") && !json.isNull("categoryid")) {
categoryId = json.getInt("categoryid")
Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId")
categoryRemoteId = json.getLong("categoryid")
} else if (json.has("categoryId") && !json.isNull("categoryId")) {
categoryRemoteId = json.getLong("categoryId")
}
if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) {
paymentModeRemoteId = json.getInt("paymentmodeid")
paymentModeRemoteId = json.getLong("paymentmodeid")
} else if (json.has("paymentModeId") && !json.isNull("paymentModeId")) {
paymentModeRemoteId = json.getLong("paymentModeId")
}
// old MB, new Cospend is ok as Cospend provides the old pm ID
// new MB, old Cospend => set payment mode ID from old one
if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) {
Log.d("PaymentMode", "old: $paymentMode and new: 0")
paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
}
val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L
val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L
val bill = DBBill(
0, remoteId, projId, payerId, amount, timestamp, what,
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeRemoteId
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId
)
bill.billOwers = getBillOwersFromJson(json, memberRemoteIdToId)
return bill
@@ -1,11 +1,12 @@
package net.helcel.cowspent.util
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import net.helcel.cowspent.model.*
import net.helcel.cowspent.model.CreditDebt
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.Transaction
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import org.json.JSONException
import org.json.JSONObject
@@ -86,14 +87,17 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int,
catId: Long, paymentModeId: Long,
dateMin: String?, dateMax: String?
): Int {
val proj = db.getProject(projId)
val categories = db.getCategories(projId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projId, proj?.remoteId)
return getStats(
db.getMembersOfProject(projId, null),
db.getBillsOfProject(projId),
membersNbBills, membersBalance, membersPaid, membersSpent,
catId, paymentModeId, dateMin, dateMax
catId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
)
}
@@ -105,7 +109,8 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int,
catId: Long, paymentModeId: Long,
reimbursementCategoryId: Long,
dateMin: String?, dateMax: String?
): Int {
val nbBillsTotal = 0
@@ -123,9 +128,9 @@ object SupportUtil {
for (b in dbBills) {
// don't take deleted bills and respect category filter
if (b.state != DBBill.STATE_DELETED &&
((catId == -1000 || catId == -100 || b.categoryRemoteId == catId) &&
(catId != -100 || b.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT) &&
(paymentModeId == -1000 || b.paymentModeRemoteId == paymentModeId)) &&
((catId == -1000L || catId == -100L || b.categoryId == catId) &&
(catId != -100L || b.categoryId != reimbursementCategoryId) &&
(paymentModeId == -1000L || b.paymentModeId == paymentModeId)) &&
(dateMin == null || b.date >= dateMin) &&
(dateMax == null || b.date <= dateMax)
) {
@@ -284,18 +289,6 @@ object SupportUtil {
return reduceBalance(crediters, debiters, results)
}
@JvmStatic
fun getVersionName(context: Context): String {
var versionName = "0.0.0"
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
versionName = pInfo.versionName ?: "0.0.0"
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return versionName
}
@JvmStatic
fun getJsonObject(text: String?): JSONObject? {
if (text == null) return null
@@ -11,8 +11,10 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
import com.nextcloud.android.sso.exceptions.TokenMismatchException
import com.nextcloud.android.sso.model.SingleSignOnAccount
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType
import org.json.JSONException
@@ -81,25 +83,18 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for getProjectInfo")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
Log.i(TAG, "using new API for getProjectInfo")
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, false), false)
}
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId
return ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
Log.i(TAG, "using public API, target is: ${target}for getProjectInfo")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
@@ -172,13 +167,9 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteProject")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
Log.i(TAG, "using new API for editRemoteProject")
ServerResponse.EditRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
@@ -190,7 +181,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
Log.i(TAG, "using public API, target is: ${target}for editRemoteProject")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
@@ -239,13 +229,9 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteMember")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members/" + member.remoteId
Log.i(TAG, "using new API for editRemoteMember")
ServerResponse.EditRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId
@@ -257,7 +243,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId
Log.i(TAG, "using public API, target is: ${target}for editRemoteMember")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members/" + member.remoteId
@@ -275,7 +260,13 @@ class VersatileProjectSyncClient(
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map<Long, Long>): ServerResponse.EditRemoteBillResponse {
fun editRemoteBill(
project: DBProject,
bill: DBBill,
memberIdToRemoteId: Map<Long, Long>,
categoryIdToRemoteId: Map<Long, Long>,
paymentModeIdToRemoteId: Map<Long, Long>
): ServerResponse.EditRemoteBillResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("date")
@@ -322,8 +313,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "")
paramValues.add(bill.categoryRemoteId.toString())
paramValues.add(bill.paymentModeRemoteId.toString())
paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) {
username = this.username
@@ -333,9 +324,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + bill.remoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteBill")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + bill.remoteId
@@ -351,7 +339,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId
Log.i(TAG, "using public API, target is: ${target}for editRemoteBill")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + bill.remoteId
@@ -389,9 +376,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteProject")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
@@ -407,7 +391,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
@@ -439,9 +422,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + billRemoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteBill")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + billRemoteId
@@ -457,7 +437,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + billRemoteId
@@ -528,9 +507,7 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects"
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for createAuthenticatedRemoteProject")
}
return ServerResponse.CreateRemoteProjectResponse(
requestServer(
target, METHOD_POST, paramKeys, paramValues,
@@ -541,7 +518,13 @@ class VersatileProjectSyncClient(
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map<Long, Long>): ServerResponse.CreateRemoteBillResponse {
fun createRemoteBill(
project: DBProject,
bill: DBBill,
memberIdToRemoteId: Map<Long, Long>,
categoryIdToRemoteId: Map<Long, Long>,
paymentModeIdToRemoteId: Map<Long, Long>
): ServerResponse.CreateRemoteBillResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("date")
@@ -583,8 +566,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "")
paramValues.add(bill.categoryRemoteId.toString())
paramValues.add(bill.paymentModeRemoteId.toString())
paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) {
username = this.username
@@ -594,9 +577,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills"
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteBill")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
@@ -612,7 +592,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills"
Log.i(TAG, "using public API, target is: ${target}for createRemoteBill")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills"
@@ -664,13 +643,9 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members"
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteMember")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
Log.i(TAG, "using new API for createRemoteBill")
ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members"
@@ -682,7 +657,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
Log.i(TAG, "using public API, target is: ${target}for createRemoteBill")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members"
@@ -717,9 +691,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills?lastchanged=" + tsLastSync
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for getBills")
}
return ServerResponse.BillsResponse(
requestServer(
target, METHOD_GET, null, null,
@@ -738,7 +709,6 @@ class VersatileProjectSyncClient(
paramValues.add(tsLastSync.toString())
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
Log.i(TAG, "using new API for getBills")
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
} else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills"
@@ -750,7 +720,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync
else
project.getRequestBaseUrl(false) + "/apiv2/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastchanged=" + tsLastSync
Log.i(TAG, "using public API, target is: ${target}for getBills")
return ServerResponse.BillsResponse(
requestServer(
target, METHOD_GET, null, null,
@@ -774,6 +743,92 @@ class VersatileProjectSyncClient(
}
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getCategories(project: DBProject): ServerResponse.CategoriesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories"
return ServerResponse.CategoriesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.CategoriesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getPaymentModes(project: DBProject): ServerResponse.PaymentModesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes"
return ServerResponse.PaymentModesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.PaymentModesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getMembers(project: DBProject): ServerResponse.MembersResponse {
var target: String
@@ -790,9 +845,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members"
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for getMembers, projectId: " + project.remoteId)
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
@@ -808,7 +860,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
Log.i(TAG, "using public API, target is: ${target}for getMembers")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members"
@@ -824,6 +875,305 @@ class VersatileProjectSyncClient(
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.CreateRemoteCategoryResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(category.name ?: "")
paramKeys.add("icon")
paramValues.add(category.icon)
paramKeys.add("color")
paramValues.add(category.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/category"
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category"
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories"
return ServerResponse.CreateRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
val response = requestServer(
target, METHOD_POST, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
)
return ServerResponse.CreateRemoteCategoryResponse(response, useOcsApiRequest)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.EditRemoteCategoryResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(category.name ?: "")
paramKeys.add("icon")
paramValues.add(category.icon)
paramKeys.add("color")
paramValues.add(category.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/category/" + category.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories/" + category.remoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category/" + category.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories/" + category.remoteId
return ServerResponse.EditRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category/" + category.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories/" + category.remoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories/" + category.remoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.EditRemoteCategoryResponse(
requestServer(
target, METHOD_PUT, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun deleteRemoteCategory(project: DBProject, categoryRemoteId: Long): ServerResponse.DeleteRemoteCategoryResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/category/" + categoryRemoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories/" + categoryRemoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category/" + categoryRemoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories/" + categoryRemoteId
return ServerResponse.DeleteRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category/" + categoryRemoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories/" + categoryRemoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories/" + categoryRemoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.DeleteRemoteCategoryResponse(
requestServer(
target, METHOD_DELETE, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemotePaymentMode(project: DBProject, paymentMode: DBPaymentMode): ServerResponse.CreateRemotePaymentModeResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(paymentMode.name ?: "")
paramKeys.add("icon")
paramValues.add(paymentMode.icon)
paramKeys.add("color")
paramValues.add(paymentMode.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/paymentmode"
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode"
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes"
return ServerResponse.CreateRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.CreateRemotePaymentModeResponse(
requestServer(
target, METHOD_POST, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemotePaymentMode(project: DBProject, paymentMode: DBPaymentMode): ServerResponse.EditRemotePaymentModeResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(paymentMode.name ?: "")
paramKeys.add("icon")
paramValues.add(paymentMode.icon)
paramKeys.add("color")
paramValues.add(paymentMode.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentMode.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentMode.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId
return ServerResponse.EditRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode/" + paymentMode.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes/" + paymentMode.remoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.EditRemotePaymentModeResponse(
requestServer(
target, METHOD_PUT, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun deleteRemotePaymentMode(project: DBProject, paymentModeRemoteId: Long): ServerResponse.DeleteRemotePaymentModeResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentModeRemoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentModeRemoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId
return ServerResponse.DeleteRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode/" + paymentModeRemoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes/" + paymentModeRemoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.DeleteRemotePaymentModeResponse(
requestServer(
target, METHOD_DELETE, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse {
val paramKeys: MutableList<String> = ArrayList()
@@ -847,9 +1197,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency"
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteCurrency")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency"
@@ -865,7 +1212,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency"
Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency"
@@ -904,9 +1250,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currency.remoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteCurrency")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currency.remoteId
@@ -922,7 +1265,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId
Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currency.remoteId
@@ -954,9 +1296,6 @@ class VersatileProjectSyncClient(
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currencyRemoteId
useOcsApiRequest = cospendVersionGT161
if (cospendVersionGT161) {
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteCurrency")
}
} else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currencyRemoteId
@@ -972,7 +1311,6 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteCurrency")
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currencyRemoteId
@@ -993,6 +1331,10 @@ class VersatileProjectSyncClient(
nextcloudAPI: NextcloudAPI, target: String, method: String,
paramKeys: List<String>?, paramValues: List<String>?, isOCSRequest: Boolean
): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
var params: MutableList<QueryParam>? = null
if (paramKeys != null && paramValues != null) {
@@ -1010,13 +1352,13 @@ class VersatileProjectSyncClient(
val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setHeader(headers)
.build()
} else {
NextcloudRequest.Builder()
.setMethod(method)
.setUrl(target)
.setUrl(finalTarget)
.setParameter(params)
.setHeader(headers)
.build()
@@ -1050,8 +1392,12 @@ class VersatileProjectSyncClient(
lastETag: String?, username: String?, password: String?,
bearerToken: String?, isOCSRequest: Boolean
): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder()
val httpCon = SupportUtil.getHttpURLConnection(target)
val httpCon = SupportUtil.getHttpURLConnection(finalTarget)
httpCon.requestMethod = method
if (bearerToken != null) {
httpCon.setRequestProperty("Authorization", "Bearer $bearerToken")
@@ -1062,7 +1408,7 @@ class VersatileProjectSyncClient(
)
}
httpCon.setRequestProperty("Connection", "Close")
httpCon.setRequestProperty("User-Agent", "Cowspent/" + SupportUtil.getAppVersionName(context))
httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context))
if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag)
}
@@ -1071,7 +1417,6 @@ class VersatileProjectSyncClient(
httpCon.setRequestProperty("Accept", "application/json")
}
httpCon.connectTimeout = 10 * 1000 // 10 seconds
Log.d(javaClass.simpleName, "$method $target")
if (paramKeys != null && paramValues != null) {
var dataString = ""
for (i in paramKeys.indices) {
@@ -1084,7 +1429,6 @@ class VersatileProjectSyncClient(
dataString += URLEncoder.encode(value, "UTF-8")
}
val data = dataString.toByteArray()
Log.d(javaClass.simpleName, "Params: $dataString")
httpCon.setFixedLengthStreamingMode(data.size)
httpCon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
httpCon.setRequestProperty("Content-Length", data.size.toString())
@@ -2,10 +2,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="100"
android:viewportHeight="100">
<!-- Matches #0F172A Base -->
<path
android:pathData="M0,0h100v100h-100z"
android:fillColor="#0F172A"/>
android:viewportWidth="72"
android:viewportHeight="72">
<group android:translateY="-0.33333334">
<group android:scaleX="0.63461536"
android:scaleY="0.63461536"
android:translateX="13.153846"
android:translateY="13.153846">
<path
android:fillColor="#FFD700"
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
<!-- Inner Face (Expanded path) -->
<path
android:fillColor="#DAA520"
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
<!-- Center Detail (Pure path replacement for the center circle) -->
<path
android:fillColor="#FFD700"
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
<!-- Massive Background Coin Base -->
<path
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
android:fillColor="#FBBF24"/>
<!-- Coin Decorative Dotted Inner Rim -->
<path
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
android:strokeColor="#D97706"
android:strokeWidth="4"/>
</group>
</group>
</vector>
@@ -9,27 +9,12 @@
android:scaleY="0.63461536"
android:translateX="13.153846"
android:translateY="13.153846">
<path
android:fillColor="#FFD700"
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
<!-- Inner Face (Expanded path) -->
<path
android:fillColor="#DAA520"
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
<!-- Center Detail (Pure path replacement for the center circle) -->
<path
android:fillColor="#FFD700"
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
<group android:name="color">
<path android:fillColor="#fff" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
<path android:fillColor="#a57939" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
<path android:fillColor="#a57939" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
<path android:fillColor="#f4aa41" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
</group>
<group android:name="line">
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
@@ -44,17 +29,6 @@
</group>
<!-- Massive Background Coin Base -->
<path
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
android:fillColor="#FBBF24"/>
<!-- Coin Decorative Dotted Inner Rim -->
<path
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
android:strokeColor="#D97706"
android:strokeWidth="4"/>
</group>
</group>
</group>
@@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="72"
android:viewportHeight="72">
<group
android:scaleX="0.63461536"
android:scaleY="0.63461536"
android:translateX="13.153846"
android:translateY="13.153846">
<!-- Cow Outlines -->
<group android:name="color">
<path android:fillColor="#000" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
<path android:fillColor="#000" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
<path android:fillColor="#000" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
<path android:fillColor="#000" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
</group>
<group android:name="line">
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m19.6191,26.2353l2.0377-9.248s-7.6806-.627-6.505-7.4454c0,0,9.9534,3.2917,12.7748.4702,0,0,6.2375-6.6642,16.1956-.0429,3.3226,2.2093,12.7748-.4702,12.7748-.4702,1.1756,6.8184-6.505,7.4454-6.505,7.4454l2.0377,9.248"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m28.4825,46h15.7265c.272,0,.5324.1138.7082.3213.8467.9992,3.1442,4.3471,2.0943,10.2966-.1836,1.0404-1.1647,1.7418-2.2037,1.5503-3.4759-.6406-11.324-1.7893-16.7592-.3263-1.1058.2976-2.2065-.454-2.3432-1.5909-.302-2.5115-.3157-6.6284,2.0103-9.8682.1751-.2438.4667-.3828.7668-.3828Z"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.8671,50.277s-.6865,2.855,3.1538,2.926"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.1276,61.4663s6.3482,7.9157,13.7936.3135"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m17.5177,20.1544c-3.0642-1.9576-7.3791-4.5873-12.4353-3.3426,0,0-1.4891,14.0288,15.3611,15.5179l1.2133.881s-2.2906,8.9177,1.2618,16.1018"/>
<path android:fillColor="#000" android:pathData="m45.8871,31.0139s3.0474-.219,3.8284-1c.7811-.781.7811-2.0474,0-2.8284-.781-.781-2.0474-.781-2.8284,0-.781.781-1,3.8284-1,3.8284Z"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m43.1817,50.2341s.6865,2.855-3.1538,2.926"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m54.7743,19.9585c3.0474-1.888,7.2683-4.4017,12.1921-3.1896,0,0,1.4891,14.0288-15.3611,15.5179l-1.2133.881s2.5382,7.9772-1.0141,15.1613"/>
</group>
</group>
</vector>
-52
View File
@@ -1,52 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:fillColor="#FFD700"
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
<!-- Inner Face (Expanded path) -->
<path
android:fillColor="#DAA520"
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
<!-- Center Detail (Pure path replacement for the center circle) -->
<path
android:fillColor="#FFD700"
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
<group android:name="color">
<path android:fillColor="#fff" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
<path android:fillColor="#a57939" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
<path android:fillColor="#a57939" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
<path android:fillColor="#f4aa41" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
</group>
<group android:name="line">
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m19.6191,26.2353l2.0377-9.248s-7.6806-.627-6.505-7.4454c0,0,9.9534,3.2917,12.7748.4702,0,0,6.2375-6.6642,16.1956-.0429,3.3226,2.2093,12.7748-.4702,12.7748-.4702,1.1756,6.8184-6.505,7.4454-6.505,7.4454l2.0377,9.248"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m28.4825,46h15.7265c.272,0,.5324.1138.7082.3213.8467.9992,3.1442,4.3471,2.0943,10.2966-.1836,1.0404-1.1647,1.7418-2.2037,1.5503-3.4759-.6406-11.324-1.7893-16.7592-.3263-1.1058.2976-2.2065-.454-2.3432-1.5909-.302-2.5115-.3157-6.6284,2.0103-9.8682.1751-.2438.4667-.3828.7668-.3828Z"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.8671,50.277s-.6865,2.855,3.1538,2.926"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.1276,61.4663s6.3482,7.9157,13.7936.3135"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m17.5177,20.1544c-3.0642-1.9576-7.3791-4.5873-12.4353-3.3426,0,0-1.4891,14.0288,15.3611,15.5179l1.2133.881s-2.2906,8.9177,1.2618,16.1018"/>
<path android:fillColor="#000" android:pathData="m45.8871,31.0139s3.0474-.219,3.8284-1c.7811-.781.7811-2.0474,0-2.8284-.781-.781-2.0474-.781-2.8284,0-.781.781-1,3.8284-1,3.8284Z"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m43.1817,50.2341s.6865,2.855-3.1538,2.926"/>
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m54.7743,19.9585c3.0474-1.888,7.2683-4.4017,12.1921-3.1896,0,0,1.4891,14.0288-15.3611,15.5179l-1.2133.881s2.5382,7.9772-1.0141,15.1613"/>
</group>
<!-- Massive Background Coin Base -->
<path
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
android:fillColor="#FBBF24"/>
<!-- Coin Decorative Dotted Inner Rim -->
<path
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
android:strokeColor="#D97706"
android:strokeWidth="4"/>
</vector>
@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">New bill</string>
<string name="action_add_project">Add project</string>
<string name="action_save">Save</string>
<string name="action_edit">Edit</string>
<string name="action_share">Share</string>
<string name="action_search">Search</string>
<string name="action_delete">Delete</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string>
<string name="action_export">Export</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Settle</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Settings</string>
<string name="action_label_bills">Label missing categories</string>
<string name="action_logout">Logout</string>
<string name="action_connect">Connect</string>
<string name="action_discard">Discard</string>
<string name="action_members">Members</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Currencies</string>
<!-- Titles -->
<string name="title_stats">Statistics</string>
<string name="title_edit_project">Edit project</string>
<string name="title_label_bills">Label Bills</string>
<string name="title_labels">Manage Labels</string>
<string name="title_about">About</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Are you sure?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">All bills</string>
<string name="label_categories">Categories</string>
<string name="label_payment_modes">Payment Modes</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Weight</string>
<string name="label_activated">Activated</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server address</string>
<string name="label_username">Username</string>
<string name="label_comment">Comment</string>
<string name="label_what">What?</string>
<string name="label_payer">Who paid?</string>
<string name="label_owers">For whom?</string>
<string name="label_repeat">Repeat every</string>
<string name="label_mode">Mode</string>
<string name="label_category">Category</string>
<string name="label_project_id">Project ID/name</string>
<string name="label_project_title">Project title</string>
<string name="label_use_sso">Use Nextcloud App Account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
<string name="dialog_sync_error_title">Sync error</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">All bills labeled</string>
<string name="msg_no_suggestions">No suggestions</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
<string name="msg_share_web">Link for web browser access.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Stats for %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Loading</string>
<string name="error_no_projects">No projects found</string>
<string name="error_no_members">No members found</string>
<string name="error_no_bills">No bills found</string>
<string name="error_no_member">At least one member required</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_400">400 Bad request</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 Forbidden</string>
<string name="error_404">404 Not Found</string>
<string name="error_sync">Sync failed: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string>
<string name="error_auth">Wrong username or password</string>
<string name="error_json">Invalid server response</string>
<string name="error_req_failed">Request failed</string>
<string name="error_invalid_email">Invalid e-mail</string>
<string name="error_invalid_project_id">Invalid project ID</string>
<string name="error_invalid_project_name">Invalid project title</string>
<string name="error_invalid_bill_name">Invalid bill name</string>
<string name="error_invalid_bill_date">Invalid bill date</string>
<string name="error_invalid_bill_payer">Payer required</string>
<string name="error_invalid_bill_owers">Owers required</string>
<string name="error_no_network">No network connection</string>
<string name="error_server">Server error</string>
<string name="error_io">Server connection broken</string>
<string name="error_share_impossible">Cannot share this project</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Yes</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<!-- Settings -->
<string name="settings_appearance">Appearance</string>
<string name="settings_network">Network</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: \"http\" is unsafe. Use \"https\".</string>
<string name="settings_colorpicker_title">Choose Color</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repeat</string>
<string name="repeat_day">Daily</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Monthly</string>
<string name="repeat_year">Yearly</string>
<string name="payment_mode_none">None</string>
<string name="payment_mode_all">All</string>
<string name="payment_mode_credit_card">Credit card</string>
<string name="payment_mode_cash">Cash</string>
<string name="payment_mode_check">Check</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">None</string>
<string name="category_all">All</string>
<string name="category_all_except_reimbursement">All except reimbursement</string>
<string name="category_groceries">Grocery</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Rent</string>
<string name="category_bills">Bill</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Health</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Reimbursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodation</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">Where</string>
<string name="where_local">Local only</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Join existing project</string>
<string name="todo_create">Create new project</string>
<string name="import_tooltip">Import from file</string>
<string name="choose_account_project_dialog_title">Choose project</string>
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project added successfully.</string>
<string name="no_projects_text">You have no projects yet.</string>
<string name="configure_account_choice">Configure Nextcloud account</string>
<string name="add_project_choice">Add project manually</string>
<string name="no_members_text">No members in this project.</string>
<string name="no_bills_text">No bills found.</string>
<string name="member_already_exists">Member already exists.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File saved: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Invalid date format at row %d</string>
<string name="import_error_owers">Invalid owers at row %d</string>
<string name="add_member_dialog_title">Add Member</string>
<string name="edit_member_dialog_title">Edit Member</string>
<string name="member_edit_delete">Delete</string>
<string name="project_edition_no_change">No changes to save.</string>
<!-- Settlement -->
<string name="center_none">None (Optimal)</string>
<string name="settle_who">Who pays</string>
<string name="settle_to_whom">To whom</string>
<string name="settle_how_much">Amount</string>
<string name="simple_settle_share">Share</string>
<string name="simple_create_bills">Create bills</string>
<string name="settle_bill_what">Settlement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string>
<string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
<string name="main_currency">Main Currency</string>
<!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string>
<string name="label_bills_skip">Skip</string>
<string name="stats_date_min">From</string>
<string name="stats_date_max">To</string>
<string name="stats_who">Member</string>
<string name="stats_paid">Paid</string>
<string name="stats_spent">Spent</string>
<string name="stats_balance">Balance</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
<string name="delete_label_confirmation_title">Delete Label</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Maintainer</string>
<string name="about_license_title">License</string>
<string name="about_source_title">Source code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">نفق</string>
<!-- Actions -->
<string name="action_new_bill">فاتورة جديدة</string>
<string name="action_add_project">إضافة مشروع</string>
<string name="action_save">حفظ</string>
<string name="action_edit">تحرير</string>
<string name="action_share">مشاركة</string>
<string name="action_search">البحث</string>
<string name="action_delete">حذف</string>
<string name="action_archive">أرشيف</string>
<string name="action_unarchive">إلغاء الأرشيف</string>
<string name="action_export">تصدير</string>
<string name="action_stats">إحصائيات</string>
<string name="action_settle">حسم</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">الإعدادات</string>
<string name="action_label_bills">فئات التسمية المفقودة</string>
<string name="action_logout">تسجيل الخروج</string>
<string name="action_connect">الاتصال</string>
<string name="action_discard">تجاهل</string>
<string name="action_members">الأعضاء</string>
<string name="action_labels">التسميات</string>
<string name="action_currencies">العملات</string>
<!-- Titles -->
<string name="title_stats">الإحصائيات</string>
<string name="title_edit_project">تحرير المشروع</string>
<string name="title_label_bills">فواتير التسمية</string>
<string name="title_labels">إدارة التسميات</string>
<string name="title_about">حول</string>
<string name="title_settle">تسوية المشروع</string>
<string name="title_share">مشاركة المشروع</string>
<string name="title_add_project">إضافة مشروع</string>
<string name="title_add_category">إضافة فئة</string>
<string name="title_add_payment_mode">إضافة وضع الدفع</string>
<string name="title_account">حساب Nextcloud</string>
<string name="title_share_web">رابط ويب</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">هل أنت متأكد؟</string>
<!-- Labels and Fields -->
<string name="label_all_bills">جميع الفواتير</string>
<string name="label_categories">الفئات</string>
<string name="label_payment_modes">طرق الدفع</string>
<string name="label_name">الاسم</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">اللون</string>
<string name="label_weight">الوزن</string>
<string name="label_activated">مُفعّل</string>
<string name="label_password">كلمة المرور</string>
<string name="label_email">البريد الإلكتروني</string>
<string name="label_url">عنوان الخادم</string>
<string name="label_username">اسم المستخدم</string>
<string name="label_comment">تعليق</string>
<string name="label_what">ماذا؟</string>
<string name="label_payer">من المدفوع؟</string>
<string name="label_owers">لمن؟</string>
<string name="label_repeat">كرر كل</string>
<string name="label_mode">الوضع</string>
<string name="label_category">الفئة</string>
<string name="label_project_id">معرف/اسم المشروع</string>
<string name="label_project_title">عنوان المشروع</string>
<string name="label_use_sso">استخدام حساب تطبيق Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">التغييرات غير المحفوظة</string>
<string name="dialog_unsaved_changes_msg">حفظ التغييرات قبل المغادرة؟</string>
<string name="dialog_confirm_remove_project_msg">لن يتم حذف المشروع البعيد.</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_balanced_msg">تم بالفعل موازنة المصروفات.</string>
<string name="msg_project_added">تم إضافة المشروع %1$s</string>
<string name="msg_bill_labeled_done">جميع الفواتير الموصوفة</string>
<string name="msg_no_suggestions">لا توجد اقتراحات</string>
<string name="msg_auth_warning">يتطلب انفاق التكلفة v0.3.4+.</string>
<string name="msg_link_copied">تم نسخ الرابط إلى الحافظة</string>
<string name="msg_share_qr">امسح رمز QR أو شارك الرابط للانضمام.</string>
<string name="msg_share_web">رابط للوصول إلى متصفح الويب.</string>
<string name="msg_share_qr_warn">شارك هذا الرابط مع مستخدم مستهلك.</string>
<string name="msg_settle_intro">التسوية ل %1$s:</string>
<string name="msg_settle_sentence">%1$s مدين %3$.2f ل %2$s</string>
<string name="msg_stats_intro">إحصائيات %1$s:</string>
<string name="msg_stats_header">عضو (رصيد مدفوع &lt;unk&gt; رصيد مدفوع)</string>
<string name="msg_logged_in_as">تم تسجيل الدخول كـ %1$s</string>
<!-- Errors -->
<string name="error_generic">خطأ</string>
<string name="error_loading">جاري التحميل</string>
<string name="error_no_projects">لم يتم العثور على مشاريع</string>
<string name="error_no_members">لم يتم العثور على أعضاء</string>
<string name="error_no_bills">لم يتم العثور على فواتير</string>
<string name="error_no_member">مطلوب عضو واحد على الأقل</string>
<string name="error_maintenance_mode">الخادم في وضع الصيانة</string>
<string name="error_400">400 طلب خاطئ</string>
<string name="error_401">401 غير مأذون</string>
<string name="error_403">403 محظور</string>
<string name="error_404">404 غير موجود</string>
<string name="error_sync">فشلت المزامنة: %1$s</string>
<string name="error_invalid_login">تسجيل الدخول غير صالح: %1$s</string>
<string name="error_auth">اسم المستخدم أو كلمة المرور خاطئة</string>
<string name="error_json">استجابة الخادم غير صالحة</string>
<string name="error_req_failed">فشل الطلب</string>
<string name="error_invalid_email">بريد إلكتروني غير صالح</string>
<string name="error_invalid_project_id">معرف المشروع غير صالح</string>
<string name="error_invalid_project_name">عنوان المشروع غير صالح</string>
<string name="error_invalid_bill_name">اسم الفاتورة غير صالح</string>
<string name="error_invalid_bill_date">تاريخ الفاتورة غير صالح</string>
<string name="error_invalid_bill_payer">الدفع مطلوب</string>
<string name="error_invalid_bill_owers">الحائزين مطلوبون</string>
<string name="error_no_network">لا يوجد اتصال بالشبكة</string>
<string name="error_server">خطأ في الخادم</string>
<string name="error_io">تم كسر اتصال الخادم</string>
<string name="error_share_impossible">لا يمكن مشاركة هذا المشروع</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">الاتصال بحساب Nextcloud</string>
<string name="drawer_last_sync">آخر مزامنة: %1$02d:%2$02d</string>
<string name="simple_cancel">إلغاء</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">حسناً</string>
<string name="simple_yes">نعم</string>
<string name="simple_no">لا</string>
<string name="simple_close">أغلق</string>
<!-- Settings -->
<string name="settings_appearance">المظهر</string>
<string name="settings_network">الشبكة</string>
<string name="settings_other">اخرى</string>
<string name="settings_night_mode">السمة</string>
<string name="settings_offline_mode">وضع غير متصل</string>
<string name="settings_offline_mode_summary">فقط المزامنة يدوياً.</string>
<string name="settings_color_custom">لون مخصص</string>
<string name="settings_color_mode">تحديد الألوان</string>
<string name="settings_show_archived">إظهار المشاريع المؤرشفة</string>
<string name="settings_beta_features">ميزات بيتا</string>
<string name="settings_beta_features_summary">تمكين الميزات التجريبية. استخدمها على مسؤوليتك الخاصة.</string>
<string name="settings_url_warn_http">تحذير: \"http\" غير آمن. استخدم \"https\".</string>
<string name="settings_colorpicker_title">اختر اللون</string>
<string name="pref_value_color_system">النظام</string>
<string name="pref_value_color_server">السحابة التالية</string>
<string name="pref_value_color_manual">دليل</string>
<string name="pref_value_theme_light">فاتح</string>
<string name="pref_value_theme_dark">داكن</string>
<string name="pref_value_theme_system">اتبع النظام</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">لا تكرّر</string>
<string name="repeat_day">يومياً</string>
<string name="repeat_week">أسبوعيا</string>
<string name="repeat_fortnight">نصف شهري</string>
<string name="repeat_month">شهريا</string>
<string name="repeat_year">سنوي</string>
<string name="payment_mode_none">لا</string>
<string name="payment_mode_all">الكل</string>
<string name="payment_mode_credit_card">بطاقة الائتمان</string>
<string name="payment_mode_cash">نقدا</string>
<string name="payment_mode_check">تحقق</string>
<string name="payment_mode_online">متصل</string>
<string name="payment_mode_transfer">نقل</string>
<string name="category_none">لا</string>
<string name="category_all">الكل</string>
<string name="category_all_except_reimbursement">جميع ما عدا رد التكاليف</string>
<string name="category_groceries">البقالة</string>
<string name="category_leisure">شريط/حفلة</string>
<string name="category_rent">إيجار</string>
<string name="category_bills">فاتورة</string>
<string name="category_excursion">الرحلة/الثقافة</string>
<string name="category_health">الصحة</string>
<string name="category_shopping">التسوق</string>
<string name="category_reimbursement">رد</string>
<string name="category_restaurant">مطعم</string>
<string name="category_accomodation">السكن</string>
<string name="category_transport">النقل</string>
<string name="category_sport">الرياضة</string>
<!-- Project specific -->
<string name="new_project_action">ماذا</string>
<string name="new_project_where">أين</string>
<string name="where_local">محلي فقط</string>
<string name="where_cospend">انفاق التكلفة</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">الانضمام إلى المشروع الحالي</string>
<string name="todo_create">إنشاء مشروع جديد</string>
<string name="import_tooltip">استيراد من الملف</string>
<string name="choose_account_project_dialog_title">اختر المشروع</string>
<string name="choose_account_project_dialog_impossible">لا توجد مشاريع في هذا الحساب.</string>
<string name="choose_project_management_action">مشروع</string>
<string name="project_added_success">تمت إضافة المشروع بنجاح.</string>
<string name="no_projects_text">ليس لديك مشاريع حتى الآن.</string>
<string name="configure_account_choice">تكوين حساب Nextcloud</string>
<string name="add_project_choice">إضافة مشروع يدوياً</string>
<string name="no_members_text">لا يوجد أعضاء في هذا المشروع.</string>
<string name="no_bills_text">لا توجد فواتير.</string>
<string name="member_already_exists">العضو موجود مسبقا.</string>
<string name="activity_dialog_title">المشروع: %1$s</string>
<string name="remove_project_confirmation">تمت إزالة المشروع %1$s.</string>
<string name="file_saved_success">تم حفظ الملف: %1$s</string>
<string name="import_error_header">فشل الاستيراد في الصف %d</string>
<string name="import_error_date">تنسيق التاريخ غير صالح في الصف %d</string>
<string name="import_error_owers">غير صالح في الصف %d</string>
<string name="add_member_dialog_title">إضافة عضو</string>
<string name="edit_member_dialog_title">تعديل العضو</string>
<string name="member_edit_delete">حذف</string>
<string name="project_edition_no_change">لا توجد تغييرات للحفظ.</string>
<!-- Settlement -->
<string name="center_none">لا شيء (مثالي)</string>
<string name="settle_who">من يدفع</string>
<string name="settle_to_whom">مَن</string>
<string name="settle_how_much">المبلغ</string>
<string name="simple_settle_share">مشاركة</string>
<string name="simple_create_bills">إنشاء فواتير</string>
<string name="settle_bill_what">تسوية</string>
<!-- Currencies -->
<string name="currency_dialog_title">اختر العملة (%s)</string>
<string name="setting_none">لا</string>
<string name="setting_all">الكل</string>
<string name="currency_saved_success">تم حفظ إعدادات العملة.</string>
<string name="main_currency">العملة الرئيسية</string>
<!-- Statistics -->
<string name="label_bills_suggested">الفئات المقترحة</string>
<string name="label_bills_skip">تخطي</string>
<string name="stats_date_min">من</string>
<string name="stats_date_max">إلى</string>
<string name="stats_who">عضو</string>
<string name="stats_paid">مدفوع</string>
<string name="stats_spent">أنفق</string>
<string name="stats_balance">الرصيد</string>
<string name="total">المجموع: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">فشل الاتصال: %1$s</string>
<string name="error_create_remote_project_helper">فشل الإنشاء: %1$s</string>
<string name="error_edit_remote_project_helper">خطأ في تحديث المشروع البعيد: %1$s</string>
<string name="remote_project_operation_no_network">الشبكة غير متوفرة للتشغيل عن بعد.</string>
<string name="error_scanning_bill_qr_code">فشل تحليل رمز QR.</string>
<string name="error_token_mismatch">عدم تطابق رمز المصادقة. الرجاء تسجيل الدخول مرة أخرى.</string>
<string name="insufficient_access_level">ليس لديك الصلاحية للقيام بهذا الإجراء.</string>
<string name="delete_label_confirmation_title">حذف التسمية</string>
<string name="delete_label_confirmation_message">هل أنت متأكد من أنك تريد حذف هذه التسمية؟</string>
<!-- About -->
<string name="about_version">الإصدار %1$s</string>
<string name="about_maintainer_title">المشرف</string>
<string name="about_license_title">الترخيص</string>
<string name="about_source_title">الكود المصدري</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">المشروع %1$s</string>
<string name="share_chooser_title">مشاركة %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">New bill</string>
<string name="action_add_project">Add project</string>
<string name="action_save">Save</string>
<string name="action_edit">Edit</string>
<string name="action_share">Share</string>
<string name="action_search">Search</string>
<string name="action_delete">Delete</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string>
<string name="action_export">Export</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Settle</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Settings</string>
<string name="action_label_bills">Label missing categories</string>
<string name="action_logout">Logout</string>
<string name="action_connect">Connect</string>
<string name="action_discard">Discard</string>
<string name="action_members">Members</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Currencies</string>
<!-- Titles -->
<string name="title_stats">Statistics</string>
<string name="title_edit_project">Edit project</string>
<string name="title_label_bills">Label Bills</string>
<string name="title_labels">Manage Labels</string>
<string name="title_about">About</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Are you sure?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">All bills</string>
<string name="label_categories">Categories</string>
<string name="label_payment_modes">Payment Modes</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Weight</string>
<string name="label_activated">Activated</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server address</string>
<string name="label_username">Username</string>
<string name="label_comment">Comment</string>
<string name="label_what">What?</string>
<string name="label_payer">Who paid?</string>
<string name="label_owers">For whom?</string>
<string name="label_repeat">Repeat every</string>
<string name="label_mode">Mode</string>
<string name="label_category">Category</string>
<string name="label_project_id">Project ID/name</string>
<string name="label_project_title">Project title</string>
<string name="label_use_sso">Use Nextcloud App Account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
<string name="dialog_sync_error_title">Sync error</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">All bills labeled</string>
<string name="msg_no_suggestions">No suggestions</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
<string name="msg_share_web">Link for web browser access.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Stats for %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Loading</string>
<string name="error_no_projects">No projects found</string>
<string name="error_no_members">No members found</string>
<string name="error_no_bills">No bills found</string>
<string name="error_no_member">At least one member required</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_400">400 Bad request</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 Forbidden</string>
<string name="error_404">404 Not Found</string>
<string name="error_sync">Sync failed: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string>
<string name="error_auth">Wrong username or password</string>
<string name="error_json">Invalid server response</string>
<string name="error_req_failed">Request failed</string>
<string name="error_invalid_email">Invalid e-mail</string>
<string name="error_invalid_project_id">Invalid project ID</string>
<string name="error_invalid_project_name">Invalid project title</string>
<string name="error_invalid_bill_name">Invalid bill name</string>
<string name="error_invalid_bill_date">Invalid bill date</string>
<string name="error_invalid_bill_payer">Payer required</string>
<string name="error_invalid_bill_owers">Owers required</string>
<string name="error_no_network">No network connection</string>
<string name="error_server">Server error</string>
<string name="error_io">Server connection broken</string>
<string name="error_share_impossible">Cannot share this project</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Yes</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<!-- Settings -->
<string name="settings_appearance">Appearance</string>
<string name="settings_network">Network</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: \"http\" is unsafe. Use \"https\".</string>
<string name="settings_colorpicker_title">Choose Color</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repeat</string>
<string name="repeat_day">Daily</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Monthly</string>
<string name="repeat_year">Yearly</string>
<string name="payment_mode_none">None</string>
<string name="payment_mode_all">All</string>
<string name="payment_mode_credit_card">Credit card</string>
<string name="payment_mode_cash">Cash</string>
<string name="payment_mode_check">Check</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">None</string>
<string name="category_all">All</string>
<string name="category_all_except_reimbursement">All except reimbursement</string>
<string name="category_groceries">Grocery</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Rent</string>
<string name="category_bills">Bill</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Health</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Reimbursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodation</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">Where</string>
<string name="where_local">Local only</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Join existing project</string>
<string name="todo_create">Create new project</string>
<string name="import_tooltip">Import from file</string>
<string name="choose_account_project_dialog_title">Choose project</string>
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project added successfully.</string>
<string name="no_projects_text">You have no projects yet.</string>
<string name="configure_account_choice">Configure Nextcloud account</string>
<string name="add_project_choice">Add project manually</string>
<string name="no_members_text">No members in this project.</string>
<string name="no_bills_text">No bills found.</string>
<string name="member_already_exists">Member already exists.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File saved: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Invalid date format at row %d</string>
<string name="import_error_owers">Invalid owers at row %d</string>
<string name="add_member_dialog_title">Add Member</string>
<string name="edit_member_dialog_title">Edit Member</string>
<string name="member_edit_delete">Delete</string>
<string name="project_edition_no_change">No changes to save.</string>
<!-- Settlement -->
<string name="center_none">None (Optimal)</string>
<string name="settle_who">Who pays</string>
<string name="settle_to_whom">To whom</string>
<string name="settle_how_much">Amount</string>
<string name="simple_settle_share">Share</string>
<string name="simple_create_bills">Create bills</string>
<string name="settle_bill_what">Settlement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string>
<string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
<string name="main_currency">Main Currency</string>
<!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string>
<string name="label_bills_skip">Skip</string>
<string name="stats_date_min">From</string>
<string name="stats_date_max">To</string>
<string name="stats_who">Member</string>
<string name="stats_paid">Paid</string>
<string name="stats_spent">Spent</string>
<string name="stats_balance">Balance</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
<string name="delete_label_confirmation_title">Delete Label</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Maintainer</string>
<string name="about_license_title">License</string>
<string name="about_source_title">Source code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Vyčerpané krávy</string>
<!-- Actions -->
<string name="action_new_bill">Nový účet</string>
<string name="action_add_project">Přidat projekt</string>
<string name="action_save">Uložit</string>
<string name="action_edit">Upravit</string>
<string name="action_share">Sdílet</string>
<string name="action_search">Hledat</string>
<string name="action_delete">Vymazat</string>
<string name="action_archive">Archiv</string>
<string name="action_unarchive">Zrušit archiv</string>
<string name="action_export">Exportovat</string>
<string name="action_stats">Statistiky</string>
<string name="action_settle">Sada</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Nastavení</string>
<string name="action_label_bills">Chybí kategorie</string>
<string name="action_logout">Odhlásit se</string>
<string name="action_connect">Připojit</string>
<string name="action_discard">Zahodit</string>
<string name="action_members">Členové</string>
<string name="action_labels">Popisky</string>
<string name="action_currencies">Měny</string>
<!-- Titles -->
<string name="title_stats">Statistiky</string>
<string name="title_edit_project">Upravit projekt</string>
<string name="title_label_bills">Etiketové účty</string>
<string name="title_labels">Spravovat štítky</string>
<string name="title_about">O aplikaci</string>
<string name="title_settle">Projekt settle</string>
<string name="title_share">Sdílet projekt</string>
<string name="title_add_project">Přidat projekt</string>
<string name="title_add_category">Přidat kategorii</string>
<string name="title_add_payment_mode">Přidat platební režim</string>
<string name="title_account">Nextcloud účet</string>
<string name="title_share_web">Webový odkaz</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Jste si jisti?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Všechny účty</string>
<string name="label_categories">Kategorie</string>
<string name="label_payment_modes">Platební režimy</string>
<string name="label_name">Název</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Barva</string>
<string name="label_weight">Hmotnost</string>
<string name="label_activated">Aktivováno</string>
<string name="label_password">Heslo</string>
<string name="label_email">E-mail</string>
<string name="label_url">Adresa serveru</string>
<string name="label_username">Uživatelské jméno</string>
<string name="label_comment">Komentář</string>
<string name="label_what">Co to znamená?</string>
<string name="label_payer">Kdo zaplatil?</string>
<string name="label_owers">Pro koho?</string>
<string name="label_repeat">Opakovat každý</string>
<string name="label_mode">Režim</string>
<string name="label_category">Kategorie</string>
<string name="label_project_id">ID / název projektu</string>
<string name="label_project_title">Název projektu</string>
<string name="label_use_sso">Použít Nextcloud účet aplikace</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Neuložené změny</string>
<string name="dialog_unsaved_changes_msg">Uložit změny před odchodem?</string>
<string name="dialog_confirm_remove_project_msg">Vzdálený projekt nebude odstraněn.</string>
<string name="dialog_sync_error_title">Chyba synchronizace</string>
<string name="dialog_sync_error_msg">Synchronizace se nezdařila pro %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Výdaje jsou již vyrovnané.</string>
<string name="msg_project_added">Projekt %1$s byl přidán</string>
<string name="msg_bill_labeled_done">Všechny účty jsou označeny</string>
<string name="msg_no_suggestions">Žádné návrhy</string>
<string name="msg_auth_warning">Vyžaduje Cospend v0.3.4+.</string>
<string name="msg_link_copied">Odkaz zkopírován do schránky</string>
<string name="msg_share_qr">Naskenujte QR kód nebo sdílejte odkaz pro připojení.</string>
<string name="msg_share_web">Odkaz pro přístup k webovému prohlížeči.</string>
<string name="msg_share_qr_warn">Sdílejte tento odkaz s Cowspent uživatelem.</string>
<string name="msg_settle_intro">Vyrovnání pro %1$s:</string>
<string name="msg_settle_sentence">%1$s dluží %3$.2f %2$s</string>
<string name="msg_stats_intro">Statistiky pro %1$s:</string>
<string name="msg_stats_header">Člen (Placené | utraceno | Zůstatek)</string>
<string name="msg_logged_in_as">Přihlášen jako %1$s</string>
<!-- Errors -->
<string name="error_generic">Chyba</string>
<string name="error_loading">Načítání</string>
<string name="error_no_projects">Nebyly nalezeny žádné projekty</string>
<string name="error_no_members">Nenalezeni žádní členové</string>
<string name="error_no_bills">Nebyly nalezeny žádné účty</string>
<string name="error_no_member">Požadován alespoň jeden člen</string>
<string name="error_maintenance_mode">Server je v režimu údržby</string>
<string name="error_400">400 špatný požadavek</string>
<string name="error_401">401 Neautorizováno</string>
<string name="error_403">403 zakázáno</string>
<string name="error_404">404 Nenalezeno</string>
<string name="error_sync">Synchronizace selhala: %1$s</string>
<string name="error_invalid_login">Neplatné přihlášení: %1$s</string>
<string name="error_auth">Nesprávné uživatelské jméno nebo heslo</string>
<string name="error_json">Neplatná odpověď serveru</string>
<string name="error_req_failed">Požadavek se nezdařil</string>
<string name="error_invalid_email">Neplatný e-mail</string>
<string name="error_invalid_project_id">Neplatné ID projektu</string>
<string name="error_invalid_project_name">Neplatný název projektu</string>
<string name="error_invalid_bill_name">Neplatné jméno účtu</string>
<string name="error_invalid_bill_date">Neplatné datum faktury</string>
<string name="error_invalid_bill_payer">Je vyžadován plátce</string>
<string name="error_invalid_bill_owers">Je vyžadován majitel</string>
<string name="error_no_network">Žádné připojení k síti</string>
<string name="error_server">Chyba serveru</string>
<string name="error_io">Připojení k serveru bylo přerušeno</string>
<string name="error_share_impossible">Nelze sdílet tento projekt</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Připojit k Nextcloud účtu</string>
<string name="drawer_last_sync">Poslední synchronizace: %1$02d:%2$02d</string>
<string name="simple_cancel">Zrušit</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Ano</string>
<string name="simple_no">Ne</string>
<string name="simple_close">Zavřít</string>
<!-- Settings -->
<string name="settings_appearance">Vzhled</string>
<string name="settings_network">Síť</string>
<string name="settings_other">Ostatní</string>
<string name="settings_night_mode">Téma</string>
<string name="settings_offline_mode">Offline režim</string>
<string name="settings_offline_mode_summary">Synchronizovat pouze ručně.</string>
<string name="settings_color_custom">Vlastní barva</string>
<string name="settings_color_mode">Výběr barvy</string>
<string name="settings_show_archived">Zobrazit archivované projekty</string>
<string name="settings_beta_features">Beta funkce</string>
<string name="settings_beta_features_summary">Povolit experimentální funkce. Používejte na vlastní riziko.</string>
<string name="settings_url_warn_http">VAROVÁNÍ: \"http\" není bezpečné. Použijte \"https://\".</string>
<string name="settings_colorpicker_title">Vybrat barvu</string>
<string name="pref_value_color_system">Systém</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Ruční</string>
<string name="pref_value_theme_light">Světlý</string>
<string name="pref_value_theme_dark">Tmavý</string>
<string name="pref_value_theme_system">Sledovat systém</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Žádné opakování</string>
<string name="repeat_day">Denní</string>
<string name="repeat_week">Týdenní</string>
<string name="repeat_fortnight">Čtvrtletní</string>
<string name="repeat_month">Měsíčně</string>
<string name="repeat_year">Roční</string>
<string name="payment_mode_none">Nic</string>
<string name="payment_mode_all">Vše</string>
<string name="payment_mode_credit_card">Kreditní karta</string>
<string name="payment_mode_cash">Hotovost</string>
<string name="payment_mode_check">Zkontrolovat</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Převod</string>
<string name="category_none">Nic</string>
<string name="category_all">Vše</string>
<string name="category_all_except_reimbursement">Všechny kromě úhrady</string>
<string name="category_groceries">Slepičí</string>
<string name="category_leisure">Čárka/Party</string>
<string name="category_rent">Pronájem</string>
<string name="category_bills">Faktura</string>
<string name="category_excursion">Výstup/kultura</string>
<string name="category_health">Zdraví</string>
<string name="category_shopping">Nakupování</string>
<string name="category_reimbursement">Náhrada</string>
<string name="category_restaurant">Restaurace</string>
<string name="category_accomodation">Ubytování</string>
<string name="category_transport">Přeprava</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Co</string>
<string name="new_project_where">Kde</string>
<string name="where_local">Pouze místní</string>
<string name="where_cospend">Koutracení</string>
<string name="where_ihatemoney">IhateMoney</string>
<string name="todo_join">Připojit se k existujícímu projektu</string>
<string name="todo_create">Vytvořit nový projekt</string>
<string name="import_tooltip">Importovat ze souboru</string>
<string name="choose_account_project_dialog_title">Vybrat projekt</string>
<string name="choose_account_project_dialog_impossible">Na tomto účtu nebyly nalezeny žádné projekty.</string>
<string name="choose_project_management_action">Projekt</string>
<string name="project_added_success">Projekt byl úspěšně přidán.</string>
<string name="no_projects_text">Zatím nemáte žádné projekty.</string>
<string name="configure_account_choice">Konfigurovat Nextcloud účet</string>
<string name="add_project_choice">Přidat projekt ručně</string>
<string name="no_members_text">Žádní členové tohoto projektu.</string>
<string name="no_bills_text">Nebyly nalezeny žádné účty.</string>
<string name="member_already_exists">Člen již existuje.</string>
<string name="activity_dialog_title">Projekt: %1$s</string>
<string name="remove_project_confirmation">Projekt %1$s byl odstraněn.</string>
<string name="file_saved_success">Soubor uložen: %1$s</string>
<string name="import_error_header">Import se nezdařil na řádku %d</string>
<string name="import_error_date">Neplatný formát data na řádku %d</string>
<string name="import_error_owers">Neplatné dlužníky na řádku %d</string>
<string name="add_member_dialog_title">Přidat člena</string>
<string name="edit_member_dialog_title">Upravit člena</string>
<string name="member_edit_delete">Vymazat</string>
<string name="project_edition_no_change">Žádné změny k uložení.</string>
<!-- Settlement -->
<string name="center_none">Žádný (optimální)</string>
<string name="settle_who">Kdo zaplatí</string>
<string name="settle_to_whom">komu</string>
<string name="settle_how_much">Částka, která má být získána</string>
<string name="simple_settle_share">Sdílet</string>
<string name="simple_create_bills">Vytvořit účty</string>
<string name="settle_bill_what">Vypořádání</string>
<!-- Currencies -->
<string name="currency_dialog_title">Vybrat měnu (%s)</string>
<string name="setting_none">Nic</string>
<string name="setting_all">Vše</string>
<string name="currency_saved_success">Nastavení měny uloženo.</string>
<string name="main_currency">Hlavní měna</string>
<!-- Statistics -->
<string name="label_bills_suggested">Navrhované kategorie</string>
<string name="label_bills_skip">Přeskočit</string>
<string name="stats_date_min">Od</string>
<string name="stats_date_max">Komu</string>
<string name="stats_who">Člen</string>
<string name="stats_paid">Zaplaceno</string>
<string name="stats_spent">Utraceno</string>
<string name="stats_balance">Zůstatek</string>
<string name="total">Celkem: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Připojení se nezdařilo: %1$s</string>
<string name="error_create_remote_project_helper">Vytvoření selhalo: %1$s</string>
<string name="error_edit_remote_project_helper">Chyba při aktualizaci vzdáleného projektu: %1$s</string>
<string name="remote_project_operation_no_network">Síť není k dispozici pro vzdálenou operaci.</string>
<string name="error_scanning_bill_qr_code">Nepodařilo se zpracovat QR kód.</string>
<string name="error_token_mismatch">Ověřovací token se neshoduje. Přihlaste se znovu.</string>
<string name="insufficient_access_level">Nemáte oprávnění k provedení této akce.</string>
<string name="delete_label_confirmation_title">Odstranit štítek</string>
<string name="delete_label_confirmation_message">Jste si jisti, že chcete odstranit tento štítek?</string>
<!-- About -->
<string name="about_version">Verze %1$s</string>
<string name="about_maintainer_title">Správce</string>
<string name="about_license_title">Licence</string>
<string name="about_source_title">Zdrojový kód</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projekt %1$s</string>
<string name="share_chooser_title">Sdílet %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Kowbrugt</string>
<!-- Actions -->
<string name="action_new_bill">Ny regning</string>
<string name="action_add_project">Tilføj projekt</string>
<string name="action_save">Gem</string>
<string name="action_edit">Rediger</string>
<string name="action_share">Del</string>
<string name="action_search">Søg</string>
<string name="action_delete">Slet</string>
<string name="action_archive">Arkiv</string>
<string name="action_unarchive">Fjern Arkiv</string>
<string name="action_export">Eksporter</string>
<string name="action_stats">Statistik</string>
<string name="action_settle">Afregn</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Indstillinger</string>
<string name="action_label_bills">Etiket mangler kategorier</string>
<string name="action_logout">Log Ud</string>
<string name="action_connect">Forbind</string>
<string name="action_discard">Kassér</string>
<string name="action_members">Medlemmer</string>
<string name="action_labels">Etiketter</string>
<string name="action_currencies">Valutaer</string>
<!-- Titles -->
<string name="title_stats">Statistik</string>
<string name="title_edit_project">Rediger projekt</string>
<string name="title_label_bills">Etiketregninger</string>
<string name="title_labels">Administrer Etiketter</string>
<string name="title_about">Om</string>
<string name="title_settle">Afregn Projekt</string>
<string name="title_share">Del Projekt</string>
<string name="title_add_project">Tilføj Projekt</string>
<string name="title_add_category">Tilføj Kategori</string>
<string name="title_add_payment_mode">Tilføj Betalingstilstand</string>
<string name="title_account">Nextcloud Konto</string>
<string name="title_share_web">Weblink</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Er du sikker?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Alle regninger</string>
<string name="label_categories">Kategorier</string>
<string name="label_payment_modes">Betalingstilstande</string>
<string name="label_name">Navn</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Farve</string>
<string name="label_weight">Vægt</string>
<string name="label_activated">Aktiveret</string>
<string name="label_password">Adgangskode</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server adresse</string>
<string name="label_username">Brugernavn</string>
<string name="label_comment">Kommentar</string>
<string name="label_what">Hvad?</string>
<string name="label_payer">Hvem betalte?</string>
<string name="label_owers">For hvem?</string>
<string name="label_repeat">Gentag hver</string>
<string name="label_mode">Tilstand</string>
<string name="label_category">Kategori</string>
<string name="label_project_id">Projekt ID/navn</string>
<string name="label_project_title">Projektets titel</string>
<string name="label_use_sso">Brug Nextcloud App-konto</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Ikke-gemte ændringer</string>
<string name="dialog_unsaved_changes_msg">Gem ændringer før afrejsen?</string>
<string name="dialog_confirm_remove_project_msg">Fjernprojektet vil ikke blive slettet.</string>
<string name="dialog_sync_error_title">Synkronisering fejl</string>
<string name="dialog_sync_error_msg">Synkronisering mislykkedes for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Udgifterne er allerede afbalancerede.</string>
<string name="msg_project_added">Projekt %1$s tilføjet</string>
<string name="msg_bill_labeled_done">Alle regninger mærket</string>
<string name="msg_no_suggestions">Ingen forslag</string>
<string name="msg_auth_warning">Kræver Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link kopieret til udklipsholder</string>
<string name="msg_share_qr">Scan QR-kode eller del linket for at deltage.</string>
<string name="msg_share_web">Link til webbrowser-adgang.</string>
<string name="msg_share_qr_warn">Del dette link med en Cowspent bruger.</string>
<string name="msg_settle_intro">Udligning for %1$s:</string>
<string name="msg_settle_sentence">%1$s skylder %3$.2f til %2$s</string>
<string name="msg_stats_intro">Statistik for %1$s:</string>
<string name="msg_stats_header">Medlem (Betalt - Spent - Balance)</string>
<string name="msg_logged_in_as">Logget ind som %1$s</string>
<!-- Errors -->
<string name="error_generic">Fejl</string>
<string name="error_loading">Indlæser</string>
<string name="error_no_projects">Ingen projekter fundet</string>
<string name="error_no_members">Ingen medlemmer fundet</string>
<string name="error_no_bills">Ingen regninger fundet</string>
<string name="error_no_member">Mindst et medlem kræves</string>
<string name="error_maintenance_mode">Serveren er i vedligeholdelsestilstand</string>
<string name="error_400">400 Dårlig forespørgsel</string>
<string name="error_401">401 Uautoriseret</string>
<string name="error_403">403 Forbudt</string>
<string name="error_404">404 Ikke Fundet</string>
<string name="error_sync">Synkronisering mislykkedes: %1$s</string>
<string name="error_invalid_login">Ugyldigt login: %1$s</string>
<string name="error_auth">Forkert brugernavn eller adgangskode</string>
<string name="error_json">Ugyldigt serversvar</string>
<string name="error_req_failed">Anmodning mislykkedes</string>
<string name="error_invalid_email">Ugyldig e-mail</string>
<string name="error_invalid_project_id">Ugyldigt projekt ID</string>
<string name="error_invalid_project_name">Ugyldig projekttitel</string>
<string name="error_invalid_bill_name">Ugyldigt regnings navn</string>
<string name="error_invalid_bill_date">Ugyldig faktureringsdato</string>
<string name="error_invalid_bill_payer">Betaler påkrævet</string>
<string name="error_invalid_bill_owers">Ejere påkrævet</string>
<string name="error_no_network">Ingen netværksforbindelse</string>
<string name="error_server">Server fejl</string>
<string name="error_io">Server forbindelse ødelagt</string>
<string name="error_share_impossible">Kan ikke dele dette projekt</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Opret forbindelse til Nextcloud konto</string>
<string name="drawer_last_sync">Seneste synkronisering: %1$02d:%2$02d</string>
<string name="simple_cancel">Annuller</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">Ja</string>
<string name="simple_no">Nej</string>
<string name="simple_close">Luk</string>
<!-- Settings -->
<string name="settings_appearance">Udseende</string>
<string name="settings_network">Netværk</string>
<string name="settings_other">Andet</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Offline tilstand</string>
<string name="settings_offline_mode_summary">Synkroniser kun manuelt.</string>
<string name="settings_color_custom">Brugerdefineret farve</string>
<string name="settings_color_mode">Valg Af Farver</string>
<string name="settings_show_archived">Vis arkiverede projekter</string>
<string name="settings_beta_features">Beta Funktioner</string>
<string name="settings_beta_features_summary">Aktiver eksperimentelle funktioner. Brug på egen risiko.</string>
<string name="settings_url_warn_http">ADVARSEL: \"http\" er usikkert. Brug \"https\".</string>
<string name="settings_colorpicker_title">Vælg Farve</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manuelt</string>
<string name="pref_value_theme_light">Lys</string>
<string name="pref_value_theme_dark">Mørk</string>
<string name="pref_value_theme_system">Følg system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Ingen gentagelse</string>
<string name="repeat_day">Dagligt</string>
<string name="repeat_week">Ugentlig</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Månedligt</string>
<string name="repeat_year">Årligt</string>
<string name="payment_mode_none">Ingen</string>
<string name="payment_mode_all">Alle</string>
<string name="payment_mode_credit_card">Kreditkort</string>
<string name="payment_mode_cash">Kontant</string>
<string name="payment_mode_check">Tjek</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Overfør</string>
<string name="category_none">Ingen</string>
<string name="category_all">Alle</string>
<string name="category_all_except_reimbursement">Alle undtagen refusion</string>
<string name="category_groceries">Køller</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Leje</string>
<string name="category_bills">Regning</string>
<string name="category_excursion">Udflugter/Kultur</string>
<string name="category_health">Sundhed</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Tilbagebetaling</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Overnatning</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Hvad</string>
<string name="new_project_where">Hvor</string>
<string name="where_local">Kun lokalt</string>
<string name="where_cospend">Cospendér</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Deltag i eksisterende projekt</string>
<string name="todo_create">Opret nyt projekt</string>
<string name="import_tooltip">Importer fra fil</string>
<string name="choose_account_project_dialog_title">Vælg projekt</string>
<string name="choose_account_project_dialog_impossible">Ingen projekter fundet på denne konto.</string>
<string name="choose_project_management_action">Projekt</string>
<string name="project_added_success">Projekt tilføjet.</string>
<string name="no_projects_text">Du har endnu ingen projekter.</string>
<string name="configure_account_choice">Konfigurer Nextcloud konto</string>
<string name="add_project_choice">Tilføj projekt manuelt</string>
<string name="no_members_text">Ingen medlemmer i dette projekt.</string>
<string name="no_bills_text">Ingen regninger fundet.</string>
<string name="member_already_exists">Medlemmet findes allerede.</string>
<string name="activity_dialog_title">Projekt: %1$s</string>
<string name="remove_project_confirmation">Projekt %1$s fjernet.</string>
<string name="file_saved_success">Fil gemt: %1$s</string>
<string name="import_error_header">Import mislykkedes ved række %d</string>
<string name="import_error_date">Ugyldigt datoformat ved række %d</string>
<string name="import_error_owers">Ugyldige ejere ved række %d</string>
<string name="add_member_dialog_title">Tilføj Medlem</string>
<string name="edit_member_dialog_title">Rediger Medlem</string>
<string name="member_edit_delete">Slet</string>
<string name="project_edition_no_change">Ingen ændringer at gemme.</string>
<!-- Settlement -->
<string name="center_none">Ingen (Optimal)</string>
<string name="settle_who">Hvem betaler</string>
<string name="settle_to_whom">Til hvem</string>
<string name="settle_how_much">Beløb</string>
<string name="simple_settle_share">Del</string>
<string name="simple_create_bills">Opret regninger</string>
<string name="settle_bill_what">Afregning</string>
<!-- Currencies -->
<string name="currency_dialog_title">Vælg Valuta (%s)</string>
<string name="setting_none">Ingen</string>
<string name="setting_all">Alle</string>
<string name="currency_saved_success">Valuta indstillinger gemt.</string>
<string name="main_currency">Primær Valuta</string>
<!-- Statistics -->
<string name="label_bills_suggested">Foreslåede Kategorier</string>
<string name="label_bills_skip">Overspring</string>
<string name="stats_date_min">Fra</string>
<string name="stats_date_max">Til</string>
<string name="stats_who">Medlem</string>
<string name="stats_paid">Betalt</string>
<string name="stats_spent">Brugt</string>
<string name="stats_balance">Saldo</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Forbindelse fejlede: %1$s</string>
<string name="error_create_remote_project_helper">Oprettelse fejlede: %1$s</string>
<string name="error_edit_remote_project_helper">Fejl under opdatering af fjernprojekt: %1$s</string>
<string name="remote_project_operation_no_network">Netværk utilgængeligt for fjernbetjening.</string>
<string name="error_scanning_bill_qr_code">Kunne ikke fortolke QR-kode.</string>
<string name="error_token_mismatch">Godkendelse token mismatch. Log venligst ind igen.</string>
<string name="insufficient_access_level">Du har ikke tilladelse til at udføre denne handling.</string>
<string name="delete_label_confirmation_title">Slet Etiket</string>
<string name="delete_label_confirmation_message">Er du sikker på, at du vil slette denne etiket?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Vedligeholder</string>
<string name="about_license_title">Licens</string>
<string name="about_source_title">Kilde kode</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projekt %1$s</string>
<string name="share_chooser_title">Del %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Kuh ausgegeben</string>
<!-- Actions -->
<string name="action_new_bill">Neue Rechnung</string>
<string name="action_add_project">Projekt hinzufügen</string>
<string name="action_save">Speichern</string>
<string name="action_edit">Bearbeiten</string>
<string name="action_share">Teilen</string>
<string name="action_search">Suchen</string>
<string name="action_delete">Löschen</string>
<string name="action_archive">Archivieren</string>
<string name="action_unarchive">Archivieren</string>
<string name="action_export">Exportieren</string>
<string name="action_stats">Statistik</string>
<string name="action_settle">Abrechnen</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Einstellungen</string>
<string name="action_label_bills">Fehlende Kategorien</string>
<string name="action_logout">Abmelden</string>
<string name="action_connect">Verbinden</string>
<string name="action_discard">Verwerfen</string>
<string name="action_members">Mitglieder</string>
<string name="action_labels">Etiketten</string>
<string name="action_currencies">Währungen</string>
<!-- Titles -->
<string name="title_stats">Statistiken</string>
<string name="title_edit_project">Projekt bearbeiten</string>
<string name="title_label_bills">Etikett Rechnungen</string>
<string name="title_labels">Labels verwalten</string>
<string name="title_about">Über</string>
<string name="title_settle">Projekt regeln</string>
<string name="title_share">Projekt teilen</string>
<string name="title_add_project">Projekt hinzufügen</string>
<string name="title_add_category">Neue Kategorie</string>
<string name="title_add_payment_mode">Zahlungsmodus hinzufügen</string>
<string name="title_account">Nächstes Cloud-Konto</string>
<string name="title_share_web">Weblink</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Bist du sicher?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Alle Rechnungen</string>
<string name="label_categories">Kategorien</string>
<string name="label_payment_modes">Zahlungsarten</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Farbe</string>
<string name="label_weight">Gewicht</string>
<string name="label_activated">Aktiviert</string>
<string name="label_password">Passwort</string>
<string name="label_email">E-Mail</string>
<string name="label_url">Serveradresse</string>
<string name="label_username">Benutzername</string>
<string name="label_comment">Kommentar</string>
<string name="label_what">Was?</string>
<string name="label_payer">Wer hat bezahlt?</string>
<string name="label_owers">Für wen?</string>
<string name="label_repeat">Wiederholen alle</string>
<string name="label_mode">Modus</string>
<string name="label_category">Kategorie</string>
<string name="label_project_id">Projekt-ID/Name</string>
<string name="label_project_title">Projekttitel</string>
<string name="label_use_sso">Nextcloud App-Konto verwenden</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Ungespeicherte Änderungen</string>
<string name="dialog_unsaved_changes_msg">Änderungen vor dem Verlassen speichern?</string>
<string name="dialog_confirm_remove_project_msg">Das entfernte Projekt wird nicht gelöscht.</string>
<string name="dialog_sync_error_title">Sync-Fehler</string>
<string name="dialog_sync_error_msg">Synchronisation fehlgeschlagen für %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Die Ausgaben sind bereits ausgeglichen.</string>
<string name="msg_project_added">Projekt %1$s hinzugefügt</string>
<string name="msg_bill_labeled_done">Alle Rechnungen markiert</string>
<string name="msg_no_suggestions">Keine Vorschläge</string>
<string name="msg_auth_warning">Benötigt Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link in Zwischenablage kopiert</string>
<string name="msg_share_qr">Scannen Sie QR-Code oder teilen Sie den Link, um beizutreten.</string>
<string name="msg_share_web">Link für den Zugriff auf Web-Browser.</string>
<string name="msg_share_qr_warn">Teilen Sie diesen Link mit einem Cowspent Benutzer.</string>
<string name="msg_settle_intro">Abrechnung für %1$s:</string>
<string name="msg_settle_sentence">%1$s schuldet %3$.2f %2$s</string>
<string name="msg_stats_intro">Statistiken für %1$s:</string>
<string name="msg_stats_header">Mitglied (Paid | Ausgaben | Guthaben)</string>
<string name="msg_logged_in_as">Angemeldet als %1$s</string>
<!-- Errors -->
<string name="error_generic">Fehler</string>
<string name="error_loading">Lädt</string>
<string name="error_no_projects">Keine Projekte gefunden</string>
<string name="error_no_members">Keine Mitglieder gefunden</string>
<string name="error_no_bills">Keine Rechnungen gefunden</string>
<string name="error_no_member">Mindestens ein Mitglied erforderlich</string>
<string name="error_maintenance_mode">Server ist im Wartungsmodus</string>
<string name="error_400">400 Falsche Anfrage</string>
<string name="error_401">401 Unautorisiert</string>
<string name="error_403">403 Verboten</string>
<string name="error_404">404 Nicht gefunden</string>
<string name="error_sync">Sync fehlgeschlagen: %1$s</string>
<string name="error_invalid_login">Ungültiger Login: %1$s</string>
<string name="error_auth">Benutzername oder Passwort falsch</string>
<string name="error_json">Ungültige Server-Antwort</string>
<string name="error_req_failed">Anfrage fehlgeschlagen</string>
<string name="error_invalid_email">Ungültige E-Mail</string>
<string name="error_invalid_project_id">Ungültige Projekt-ID</string>
<string name="error_invalid_project_name">Ungültiger Projekttitel</string>
<string name="error_invalid_bill_name">Ungültiger Rechnungsname</string>
<string name="error_invalid_bill_date">Ungültiges Rechnungsdatum</string>
<string name="error_invalid_bill_payer">Zahler erforderlich</string>
<string name="error_invalid_bill_owers">Eigene erforderlich</string>
<string name="error_no_network">Keine Netzwerkverbindung</string>
<string name="error_server">Serverfehler</string>
<string name="error_io">Serververbindung defekt</string>
<string name="error_share_impossible">Dieses Projekt kann nicht geteilt werden</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Mit Nextcloud Konto verbinden</string>
<string name="drawer_last_sync">Letzter Sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Abbrechen</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Ja</string>
<string name="simple_no">Nein</string>
<string name="simple_close">Schließen</string>
<!-- Settings -->
<string name="settings_appearance">Erscheinung</string>
<string name="settings_network">Netzwerk</string>
<string name="settings_other">Andere</string>
<string name="settings_night_mode">Thema</string>
<string name="settings_offline_mode">Offline-Modus</string>
<string name="settings_offline_mode_summary">Nur manuell synchronisieren.</string>
<string name="settings_color_custom">Eigene Farbe</string>
<string name="settings_color_mode">Farbauswahl</string>
<string name="settings_show_archived">Archivierte Projekte anzeigen</string>
<string name="settings_beta_features">Beta-Funktionen</string>
<string name="settings_beta_features_summary">Experimentelle Funktionen aktivieren. Benutzung auf eigene Gefahr.</string>
<string name="settings_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwenden Sie \"https\".</string>
<string name="settings_colorpicker_title">Farbe wählen</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nächste Cloud</string>
<string name="pref_value_color_manual">Manuell</string>
<string name="pref_value_theme_light">Hell</string>
<string name="pref_value_theme_dark">Dunkel</string>
<string name="pref_value_theme_system">System folgen</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Keine Wiederholung</string>
<string name="repeat_day">Täglich</string>
<string name="repeat_week">Wöchentlich</string>
<string name="repeat_fortnight">Vierzehn</string>
<string name="repeat_month">Monatlich</string>
<string name="repeat_year">Jährlich</string>
<string name="payment_mode_none">Keine</string>
<string name="payment_mode_all">Alle</string>
<string name="payment_mode_credit_card">Kreditkarte</string>
<string name="payment_mode_cash">Bargeld</string>
<string name="payment_mode_check">Prüfen</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Überweisung</string>
<string name="category_none">Keine</string>
<string name="category_all">Alle</string>
<string name="category_all_except_reimbursement">Alle außer Rückerstattung</string>
<string name="category_groceries">Fleischgerichte</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Miete</string>
<string name="category_bills">Abrechnung</string>
<string name="category_excursion">Ausflug/Kultur</string>
<string name="category_health">Gesundheit</string>
<string name="category_shopping">Einkaufen</string>
<string name="category_reimbursement">Rückzahlung</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Unterkunft</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Was</string>
<string name="new_project_where">Wo</string>
<string name="where_local">Nur lokal</string>
<string name="where_cospend">Ko-Ausgaben</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Bestehendes Projekt beitreten</string>
<string name="todo_create">Neues Projekt erstellen</string>
<string name="import_tooltip">Aus Datei importieren</string>
<string name="choose_account_project_dialog_title">Projekt auswählen</string>
<string name="choose_account_project_dialog_impossible">Keine Projekte auf diesem Konto gefunden.</string>
<string name="choose_project_management_action">Projekt</string>
<string name="project_added_success">Projekt erfolgreich hinzugefügt.</string>
<string name="no_projects_text">Sie haben noch keine Projekte.</string>
<string name="configure_account_choice">Nextcloud-Konto konfigurieren</string>
<string name="add_project_choice">Projekt manuell hinzufügen</string>
<string name="no_members_text">Keine Mitglieder in diesem Projekt.</string>
<string name="no_bills_text">Keine Rechnungen gefunden.</string>
<string name="member_already_exists">Mitglied existiert bereits.</string>
<string name="activity_dialog_title">Projekt: %1$s</string>
<string name="remove_project_confirmation">Projekt %1$s entfernt.</string>
<string name="file_saved_success">Datei gespeichert: %1$s</string>
<string name="import_error_header">Import fehlgeschlagen in Zeile %d</string>
<string name="import_error_date">Ungültiges Datumsformat in Zeile %d</string>
<string name="import_error_owers">Ungültige Eigener in Zeile %d</string>
<string name="add_member_dialog_title">Mitglied hinzufügen</string>
<string name="edit_member_dialog_title">Mitglied bearbeiten</string>
<string name="member_edit_delete">Löschen</string>
<string name="project_edition_no_change">Keine zu speichernden Änderungen.</string>
<!-- Settlement -->
<string name="center_none">Keine (optimal)</string>
<string name="settle_who">Wer zahlt</string>
<string name="settle_to_whom">An wen</string>
<string name="settle_how_much">Betrag</string>
<string name="simple_settle_share">Teilen</string>
<string name="simple_create_bills">Rechnungen erstellen</string>
<string name="settle_bill_what">Abrechnung</string>
<!-- Currencies -->
<string name="currency_dialog_title">Währung wählen (%s)</string>
<string name="setting_none">Keine</string>
<string name="setting_all">Alle</string>
<string name="currency_saved_success">Währungseinstellungen gespeichert.</string>
<string name="main_currency">Hauptwährung</string>
<!-- Statistics -->
<string name="label_bills_suggested">Empfohlene Kategorien</string>
<string name="label_bills_skip">Überspringen</string>
<string name="stats_date_min">Von</string>
<string name="stats_date_max">An</string>
<string name="stats_who">Mitglied</string>
<string name="stats_paid">Bezahlt</string>
<string name="stats_spent">Ausgegeben</string>
<string name="stats_balance">Saldo</string>
<string name="total">Gesamt: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Verbindung fehlgeschlagen: %1$s</string>
<string name="error_create_remote_project_helper">Erstellung fehlgeschlagen: %1$s</string>
<string name="error_edit_remote_project_helper">Fehler beim Aktualisieren des fernen Projekts: %1$s</string>
<string name="remote_project_operation_no_network">Netzwerk nicht verfügbar für Remote Operation.</string>
<string name="error_scanning_bill_qr_code">Fehler beim Analysieren des QR-Codes.</string>
<string name="error_token_mismatch">Authentifizierungstoken stimmen nicht überein. Bitte melden Sie sich erneut an.</string>
<string name="insufficient_access_level">Sie haben keine Berechtigung, um diese Aktion durchzuführen.</string>
<string name="delete_label_confirmation_title">Label löschen</string>
<string name="delete_label_confirmation_message">Sind Sie sicher, dass Sie diese Bezeichnung löschen möchten?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Betreuer</string>
<string name="about_license_title">Lizenz</string>
<string name="about_source_title">Quellcode</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projekt %1$s</string>
<string name="share_chooser_title">Teilen %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Έλαιο</string>
<!-- Actions -->
<string name="action_new_bill">Νέος λογαριασμός</string>
<string name="action_add_project">Προσθήκη έργου</string>
<string name="action_save">Αποθήκευση</string>
<string name="action_edit">Επεξεργασία</string>
<string name="action_share">Κοινοποίηση</string>
<string name="action_search">Αναζήτηση</string>
<string name="action_delete">Διαγραφή</string>
<string name="action_archive">Αρχειοθέτηση</string>
<string name="action_unarchive">Αποσυμπιεσμένο</string>
<string name="action_export">Εξαγωγή</string>
<string name="action_stats">Στατιστικά</string>
<string name="action_settle">Διευθέτηση</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Ρυθμίσεις</string>
<string name="action_label_bills">Λείπει η ετικέτα κατηγορίες</string>
<string name="action_logout">Αποσύνδεση</string>
<string name="action_connect">Σύνδεση</string>
<string name="action_discard">Απορρίψτε</string>
<string name="action_members">Μέλη</string>
<string name="action_labels">Ετικέτες</string>
<string name="action_currencies">Νομίσματα</string>
<!-- Titles -->
<string name="title_stats">Στατιστικά</string>
<string name="title_edit_project">Επεξεργασία έργου</string>
<string name="title_label_bills">Λογαριασμοί Ετικέτας</string>
<string name="title_labels">Διαχείριση Ετικετών</string>
<string name="title_about">Σχετικά</string>
<string name="title_settle">Διευθέτηση Έργου</string>
<string name="title_share">Μοιραστείτε Το Έργο</string>
<string name="title_add_project">Προσθήκη Έργου</string>
<string name="title_add_category">Προσθήκη Κατηγορίας</string>
<string name="title_add_payment_mode">Προσθήκη Λειτουργίας Πληρωμής</string>
<string name="title_account">Λογαριασμός Nextcloud</string>
<string name="title_share_web">Σύνδεσμος ιστού</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Είσαι σίγουρος?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Όλοι οι λογαριασμοί</string>
<string name="label_categories">Κατηγορίες</string>
<string name="label_payment_modes">Τρόποι Πληρωμής</string>
<string name="label_name">Όνομα</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Χρώμα</string>
<string name="label_weight">Βάρος</string>
<string name="label_activated">Ενεργοποιήθηκε</string>
<string name="label_password">Κωδικός</string>
<string name="label_email">E-mail</string>
<string name="label_url">Διεύθυνση διακομιστή</string>
<string name="label_username">Όνομα Χρήστη</string>
<string name="label_comment">Σχόλιο</string>
<string name="label_what">Τι Είναι?</string>
<string name="label_payer">Ποιος πλήρωσε?</string>
<string name="label_owers">Για ποιον?</string>
<string name="label_repeat">Επανάληψη κάθε</string>
<string name="label_mode">Λειτουργία</string>
<string name="label_category">Κατηγορία</string>
<string name="label_project_id">Ταυτότητα/όνομα Έργου</string>
<string name="label_project_title">Τίτλος έργου</string>
<string name="label_use_sso">Χρήση Λογαριασμού Εφαρμογής Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Μη αποθηκευμένες αλλαγές</string>
<string name="dialog_unsaved_changes_msg">Αποθήκευση αλλαγών πριν την αναχώρηση?</string>
<string name="dialog_confirm_remove_project_msg">Το απομακρυσμένο έργο δεν θα διαγραφεί.</string>
<string name="dialog_sync_error_title">Σφάλμα συγχρονισμού</string>
<string name="dialog_sync_error_msg">Ο Συγχρονισμός απέτυχε για %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Οι δαπάνες είναι ήδη ισορροπημένες.</string>
<string name="msg_project_added">Το έργο %1$s προστέθηκε</string>
<string name="msg_bill_labeled_done">Όλοι οι λογαριασμοί με επισήμανση</string>
<string name="msg_no_suggestions">Δεν υπάρχουν προτάσεις</string>
<string name="msg_auth_warning">Απαιτεί Cospend v0.3.4+.</string>
<string name="msg_link_copied">Ο σύνδεσμος αντιγράφηκε στο πρόχειρο</string>
<string name="msg_share_qr">Σαρώστε τον κωδικό QR ή μοιραστείτε το σύνδεσμο για να ενταχθείτε.</string>
<string name="msg_share_web">Σύνδεσμος για πρόσβαση στο πρόγραμμα περιήγησης ιστού.</string>
<string name="msg_share_qr_warn">Μοιραστείτε αυτό το σύνδεσμο με έναν χρήστη Cowspent .</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_stats_intro">Στατιστικά για %1$s:</string>
<string name="msg_stats_header">Μέλος (Πάροχος (Ισορροπία)</string>
<string name="msg_logged_in_as">Συνδεδεμένος ως %1$s</string>
<!-- Errors -->
<string name="error_generic">Σφάλμα</string>
<string name="error_loading">Φόρτωση</string>
<string name="error_no_projects">Δεν βρέθηκαν πρότζεκτ</string>
<string name="error_no_members">Δεν βρέθηκαν μέλη</string>
<string name="error_no_bills">Δεν βρέθηκαν λογαριασμοί</string>
<string name="error_no_member">Απαιτείται τουλάχιστον ένα μέλος</string>
<string name="error_maintenance_mode">Ο διακομιστής είναι σε λειτουργία συντήρησης</string>
<string name="error_400">400 Εσφαλμένο αίτημα</string>
<string name="error_401">401 Μη Εξουσιοδοτημένο</string>
<string name="error_403">403 Απαγορευμένο</string>
<string name="error_404">404 Δεν Βρέθηκε</string>
<string name="error_sync">Συγχρονισμός απέτυχε: %1$s</string>
<string name="error_invalid_login">Μη έγκυρη σύνδεση: %1$s</string>
<string name="error_auth">Λάθος όνομα χρήστη ή κωδικός πρόσβασης</string>
<string name="error_json">Μη έγκυρη απάντηση διακομιστή</string>
<string name="error_req_failed">Το αίτημα απέτυχε</string>
<string name="error_invalid_email">Μη έγκυρο e-mail</string>
<string name="error_invalid_project_id">Μη έγκυρο ID έργου</string>
<string name="error_invalid_project_name">Μη έγκυρος τίτλος έργου</string>
<string name="error_invalid_bill_name">Μη έγκυρο όνομα λογαριασμού</string>
<string name="error_invalid_bill_date">Μη έγκυρη ημερομηνία λογαριασμού</string>
<string name="error_invalid_bill_payer">Απαιτείται πληρωτής</string>
<string name="error_invalid_bill_owers">Απαιτούνται ιδιοκτήτες</string>
<string name="error_no_network">Χωρίς σύνδεση δικτύου</string>
<string name="error_server">Σφάλμα διακομιστή</string>
<string name="error_io">Η σύνδεση διακομιστή έσπασε</string>
<string name="error_share_impossible">Αδύνατη η κοινοποίηση αυτού του έργου</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Συνδεθείτε στο λογαριασμό Nextcloud</string>
<string name="drawer_last_sync">Τελευταίος συγχρονισμός: %1$02d:%2$02d</string>
<string name="simple_cancel">Ακύρωση</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Εντάξει</string>
<string name="simple_yes">Ναι</string>
<string name="simple_no">Όχι</string>
<string name="simple_close">Κλείσιμο</string>
<!-- Settings -->
<string name="settings_appearance">Εμφάνιση</string>
<string name="settings_network">Δίκτυο</string>
<string name="settings_other">Άλλο</string>
<string name="settings_night_mode">Θέμα</string>
<string name="settings_offline_mode">Λειτουργία χωρίς σύνδεση</string>
<string name="settings_offline_mode_summary">Μόνο χειροκίνητος συγχρονισμός.</string>
<string name="settings_color_custom">Προσαρμοσμένο χρώμα</string>
<string name="settings_color_mode">Επιλογή Χρώματος</string>
<string name="settings_show_archived">Εμφάνιση αρχειοθετημένων έργων</string>
<string name="settings_beta_features">Χαρακτηριστικά Beta</string>
<string name="settings_beta_features_summary">Ενεργοποιήσετε πειραματικά χαρακτηριστικά. Χρησιμοποιήστε το με δική σας ευθύνη.</string>
<string name="settings_url_warn_http">ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Το \"http\" δεν είναι ασφαλές. Χρησιμοποιήστε το \"Ř\".</string>
<string name="settings_colorpicker_title">Επιλογή Χρώματος</string>
<string name="pref_value_color_system">Σύστημα</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Χειροκίνητα</string>
<string name="pref_value_theme_light">Φωτεινό</string>
<string name="pref_value_theme_dark">Σκοτεινό</string>
<string name="pref_value_theme_system">Ακολουθήστε το σύστημα</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Χωρίς επανάληψη</string>
<string name="repeat_day">Καθημερινά</string>
<string name="repeat_week">Εβδομαδιαία</string>
<string name="repeat_fortnight">Δεκαπενθήμερα</string>
<string name="repeat_month">Μηνιαία</string>
<string name="repeat_year">Ετήσια</string>
<string name="payment_mode_none">Κανένα</string>
<string name="payment_mode_all">Όλα</string>
<string name="payment_mode_credit_card">Πιστωτική κάρτα</string>
<string name="payment_mode_cash">Μετρητά</string>
<string name="payment_mode_check">Έλεγχος</string>
<string name="payment_mode_online">Συνδεδεμένος</string>
<string name="payment_mode_transfer">Μεταφορά</string>
<string name="category_none">Κανένα</string>
<string name="category_all">Όλα</string>
<string name="category_all_except_reimbursement">Όλα εκτός από την επιστροφή</string>
<string name="category_groceries">Παντοπωλείο</string>
<string name="category_leisure">Μπαρ/Πάρτι</string>
<string name="category_rent">Ενοίκιο</string>
<string name="category_bills">Λογαριασμός</string>
<string name="category_excursion">Εκδρομή/Πολιτισμός</string>
<string name="category_health">Υγεία</string>
<string name="category_shopping">Αγορές</string>
<string name="category_reimbursement">Επιστροφή</string>
<string name="category_restaurant">Εστιατόριο</string>
<string name="category_accomodation">Διαμονή</string>
<string name="category_transport">Μεταφορές</string>
<string name="category_sport">Αθλητισμός</string>
<!-- Project specific -->
<string name="new_project_action">Τι</string>
<string name="new_project_where">Όπου</string>
<string name="where_local">Τοπικό μόνο</string>
<string name="where_cospend">Κόστος</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Συμμετοχή σε υπάρχον έργο</string>
<string name="todo_create">Δημιουργία νέου έργου</string>
<string name="import_tooltip">Εισαγωγή από αρχείο</string>
<string name="choose_account_project_dialog_title">Επιλογή έργου</string>
<string name="choose_account_project_dialog_impossible">Δεν βρέθηκαν έργα σε αυτόν τον λογαριασμό.</string>
<string name="choose_project_management_action">Έργο</string>
<string name="project_added_success">Το έργο προστέθηκε επιτυχώς.</string>
<string name="no_projects_text">Δεν έχετε ακόμα πρότζεκτ.</string>
<string name="configure_account_choice">Ρύθμιση λογαριασμού Nextcloud</string>
<string name="add_project_choice">Χειροκίνητη προσθήκη έργου</string>
<string name="no_members_text">Κανένα μέλος σε αυτό το έργο.</string>
<string name="no_bills_text">Δεν βρέθηκαν λογαριασμοί.</string>
<string name="member_already_exists">Το μέλος υπάρχει ήδη.</string>
<string name="activity_dialog_title">Έργο: %1$s</string>
<string name="remove_project_confirmation">Το έργο %1$s αφαιρέθηκε.</string>
<string name="file_saved_success">Το αρχείο αποθηκεύτηκε: %1$s</string>
<string name="import_error_header">Η εισαγωγή απέτυχε στη γραμμή %d</string>
<string name="import_error_date">Μη έγκυρη μορφή ημερομηνίας στη γραμμή %d</string>
<string name="import_error_owers">Μη έγκυρες owers στη σειρά %d</string>
<string name="add_member_dialog_title">Προσθήκη Μελών</string>
<string name="edit_member_dialog_title">Επεξεργασία Μέλους</string>
<string name="member_edit_delete">Διαγραφή</string>
<string name="project_edition_no_change">Δεν υπάρχουν αλλαγές για αποθήκευση.</string>
<!-- Settlement -->
<string name="center_none">Κανένα (Βέλτιστο Μέρος)</string>
<string name="settle_who">Ποιος πληρώνει</string>
<string name="settle_to_whom">Σε ποιους</string>
<string name="settle_how_much">Ποσό</string>
<string name="simple_settle_share">Κοινοποίηση</string>
<string name="simple_create_bills">Δημιουργία λογαριασμών</string>
<string name="settle_bill_what">Διευθέτηση</string>
<!-- Currencies -->
<string name="currency_dialog_title">Επιλέξτε Νόμισμα (%s)</string>
<string name="setting_none">Κανένα</string>
<string name="setting_all">Όλα</string>
<string name="currency_saved_success">Οι ρυθμίσεις νομίσματος αποθηκεύτηκαν.</string>
<string name="main_currency">Κύριο Νόμισμα</string>
<!-- Statistics -->
<string name="label_bills_suggested">Προτεινόμενες Κατηγορίες</string>
<string name="label_bills_skip">Παράλειψη</string>
<string name="stats_date_min">Από</string>
<string name="stats_date_max">Προς</string>
<string name="stats_who">Κράτος</string>
<string name="stats_paid">Πληρωμένο</string>
<string name="stats_spent">Δαπανήθηκε</string>
<string name="stats_balance">Υπόλοιπο</string>
<string name="total">Σύνολο: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Αποτυχία σύνδεσης: %1$s</string>
<string name="error_create_remote_project_helper">Η δημιουργία απέτυχε: %1$s</string>
<string name="error_edit_remote_project_helper">Σφάλμα ενημέρωσης απομακρυσμένου έργου: %1$s</string>
<string name="remote_project_operation_no_network">Το δίκτυο δεν είναι διαθέσιμο για απομακρυσμένη λειτουργία.</string>
<string name="error_scanning_bill_qr_code">Αποτυχία ανάλυσης QR code.</string>
<string name="error_token_mismatch">Ασυμφωνία διακριτικού ταυτοποίησης. Παρακαλώ συνδεθείτε ξανά.</string>
<string name="insufficient_access_level">Δεν έχετε άδεια για να εκτελέσετε αυτήν την ενέργεια.</string>
<string name="delete_label_confirmation_title">Διαγραφή Ετικέτας</string>
<string name="delete_label_confirmation_message">Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή την ετικέτα?</string>
<!-- About -->
<string name="about_version">Έκδοση %1$s</string>
<string name="about_maintainer_title">Συντηρητής</string>
<string name="about_license_title">Άδεια</string>
<string name="about_source_title">Πηγαίος κώδικας</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Έργο %1$s</string>
<string name="share_chooser_title">Κοινοποίηση %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">New bill</string>
<string name="action_add_project">Add project</string>
<string name="action_save">Save</string>
<string name="action_edit">Edit</string>
<string name="action_share">Share</string>
<string name="action_search">Search</string>
<string name="action_delete">Delete</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string>
<string name="action_export">Export</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Settle</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Settings</string>
<string name="action_label_bills">Label missing categories</string>
<string name="action_logout">Logout</string>
<string name="action_connect">Connect</string>
<string name="action_discard">Discard</string>
<string name="action_members">Members</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Currencies</string>
<!-- Titles -->
<string name="title_stats">Statistics</string>
<string name="title_edit_project">Edit project</string>
<string name="title_label_bills">Label Bills</string>
<string name="title_labels">Manage Labels</string>
<string name="title_about">About</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Are you sure?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">All bills</string>
<string name="label_categories">Categories</string>
<string name="label_payment_modes">Payment Modes</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Weight</string>
<string name="label_activated">Activated</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server address</string>
<string name="label_username">Username</string>
<string name="label_comment">Comment</string>
<string name="label_what">What?</string>
<string name="label_payer">Who paid?</string>
<string name="label_owers">For whom?</string>
<string name="label_repeat">Repeat every</string>
<string name="label_mode">Mode</string>
<string name="label_category">Category</string>
<string name="label_project_id">Project ID/name</string>
<string name="label_project_title">Project title</string>
<string name="label_use_sso">Use Nextcloud App Account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
<string name="dialog_sync_error_title">Sync error</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">All bills labeled</string>
<string name="msg_no_suggestions">No suggestions</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
<string name="msg_share_web">Link for web browser access.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Stats for %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Loading</string>
<string name="error_no_projects">No projects found</string>
<string name="error_no_members">No members found</string>
<string name="error_no_bills">No bills found</string>
<string name="error_no_member">At least one member required</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_400">400 Bad request</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 Forbidden</string>
<string name="error_404">404 Not Found</string>
<string name="error_sync">Sync failed: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string>
<string name="error_auth">Wrong username or password</string>
<string name="error_json">Invalid server response</string>
<string name="error_req_failed">Request failed</string>
<string name="error_invalid_email">Invalid e-mail</string>
<string name="error_invalid_project_id">Invalid project ID</string>
<string name="error_invalid_project_name">Invalid project title</string>
<string name="error_invalid_bill_name">Invalid bill name</string>
<string name="error_invalid_bill_date">Invalid bill date</string>
<string name="error_invalid_bill_payer">Payer required</string>
<string name="error_invalid_bill_owers">Owers required</string>
<string name="error_no_network">No network connection</string>
<string name="error_server">Server error</string>
<string name="error_io">Server connection broken</string>
<string name="error_share_impossible">Cannot share this project</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Yes</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<!-- Settings -->
<string name="settings_appearance">Appearance</string>
<string name="settings_network">Network</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: \"http\" is unsafe. Use \"https\".</string>
<string name="settings_colorpicker_title">Choose Color</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repeat</string>
<string name="repeat_day">Daily</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Monthly</string>
<string name="repeat_year">Yearly</string>
<string name="payment_mode_none">None</string>
<string name="payment_mode_all">All</string>
<string name="payment_mode_credit_card">Credit card</string>
<string name="payment_mode_cash">Cash</string>
<string name="payment_mode_check">Check</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">None</string>
<string name="category_all">All</string>
<string name="category_all_except_reimbursement">All except reimbursement</string>
<string name="category_groceries">Grocery</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Rent</string>
<string name="category_bills">Bill</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Health</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Reimbursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodation</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">Where</string>
<string name="where_local">Local only</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Join existing project</string>
<string name="todo_create">Create new project</string>
<string name="import_tooltip">Import from file</string>
<string name="choose_account_project_dialog_title">Choose project</string>
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project added successfully.</string>
<string name="no_projects_text">You have no projects yet.</string>
<string name="configure_account_choice">Configure Nextcloud account</string>
<string name="add_project_choice">Add project manually</string>
<string name="no_members_text">No members in this project.</string>
<string name="no_bills_text">No bills found.</string>
<string name="member_already_exists">Member already exists.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File saved: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Invalid date format at row %d</string>
<string name="import_error_owers">Invalid owers at row %d</string>
<string name="add_member_dialog_title">Add Member</string>
<string name="edit_member_dialog_title">Edit Member</string>
<string name="member_edit_delete">Delete</string>
<string name="project_edition_no_change">No changes to save.</string>
<!-- Settlement -->
<string name="center_none">None (Optimal)</string>
<string name="settle_who">Who pays</string>
<string name="settle_to_whom">To whom</string>
<string name="settle_how_much">Amount</string>
<string name="simple_settle_share">Share</string>
<string name="simple_create_bills">Create bills</string>
<string name="settle_bill_what">Settlement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string>
<string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
<string name="main_currency">Main Currency</string>
<!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string>
<string name="label_bills_skip">Skip</string>
<string name="stats_date_min">From</string>
<string name="stats_date_max">To</string>
<string name="stats_who">Member</string>
<string name="stats_paid">Paid</string>
<string name="stats_spent">Spent</string>
<string name="stats_balance">Balance</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
<string name="delete_label_confirmation_title">Delete Label</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Maintainer</string>
<string name="about_license_title">License</string>
<string name="about_source_title">Source code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Capucha gastada</string>
<!-- Actions -->
<string name="action_new_bill">Nueva factura</string>
<string name="action_add_project">Añadir proyecto</string>
<string name="action_save">Guardar</string>
<string name="action_edit">Editar</string>
<string name="action_share">Compartir</string>
<string name="action_search">Buscar</string>
<string name="action_delete">Eliminar</string>
<string name="action_archive">Archivar</string>
<string name="action_unarchive">Desarchivar</string>
<string name="action_export">Exportar</string>
<string name="action_stats">Estadísticas</string>
<string name="action_settle">Salir</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Ajustes</string>
<string name="action_label_bills">Categorías faltantes de etiqueta</string>
<string name="action_logout">Cerrar sesión</string>
<string name="action_connect">Conectar</string>
<string name="action_discard">Descartar</string>
<string name="action_members">Miembros</string>
<string name="action_labels">Etiquetas</string>
<string name="action_currencies">Monedas</string>
<!-- Titles -->
<string name="title_stats">Estadísticas</string>
<string name="title_edit_project">Editar proyecto</string>
<string name="title_label_bills">Facturas de etiqueta</string>
<string name="title_labels">Administrar etiquetas</string>
<string name="title_about">Acerca de</string>
<string name="title_settle">Liquidar Proyecto</string>
<string name="title_share">Compartir proyecto</string>
<string name="title_add_project">Añadir proyecto</string>
<string name="title_add_category">Añadir categoría</string>
<string name="title_add_payment_mode">Añadir modo de pago</string>
<string name="title_account">Cuenta Nextcloud</string>
<string name="title_share_web">Enlace web</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">¿Estás seguro?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Todas las facturas</string>
<string name="label_categories">Categorías</string>
<string name="label_payment_modes">Modos de pago</string>
<string name="label_name">Nombre</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Peso</string>
<string name="label_activated">Activado</string>
<string name="label_password">Contraseña</string>
<string name="label_email">E-mail</string>
<string name="label_url">Dirección del servidor</string>
<string name="label_username">Usuario</string>
<string name="label_comment">Comentario</string>
<string name="label_what">¿Qué?</string>
<string name="label_payer">¿Quién pagó?</string>
<string name="label_owers">¿Para quién?</string>
<string name="label_repeat">Repetir cada</string>
<string name="label_mode">Modo</string>
<string name="label_category">Categoría</string>
<string name="label_project_id">ID del proyecto/nombre</string>
<string name="label_project_title">Título del proyecto</string>
<string name="label_use_sso">Usar cuenta de la aplicación Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Cambios sin guardar</string>
<string name="dialog_unsaved_changes_msg">¿Guardar cambios antes de salir?</string>
<string name="dialog_confirm_remove_project_msg">El proyecto remoto no se eliminará.</string>
<string name="dialog_sync_error_title">Error de sincronización</string>
<string name="dialog_sync_error_msg">Sincronización fallida para %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Los gastos ya están equilibrados.</string>
<string name="msg_project_added">Proyecto %1$s añadido</string>
<string name="msg_bill_labeled_done">Todas las facturas etiquetadas</string>
<string name="msg_no_suggestions">No hay sugerencias</string>
<string name="msg_auth_warning">Requiere Gasto v0.3.4+.</string>
<string name="msg_link_copied">Enlace copiado al portapapeles</string>
<string name="msg_share_qr">Escanea el código QR o comparte el enlace para unirte.</string>
<string name="msg_share_web">Enlace para acceso al navegador web.</string>
<string name="msg_share_qr_warn">Comparte este enlace con un usuario de Cowged.</string>
<string name="msg_settle_intro">Acuerdo para %1$s:</string>
<string name="msg_settle_sentence">%1$s debe %3$.2f a %2$s</string>
<string name="msg_stats_intro">Estadísticas de %1$s:</string>
<string name="msg_stats_header">Miembro Pagado | Gasto | Saldo)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Cargando</string>
<string name="error_no_projects">No se encontraron proyectos</string>
<string name="error_no_members">Ningún miembro encontrado</string>
<string name="error_no_bills">No se encontraron facturas</string>
<string name="error_no_member">Se requiere al menos un miembro</string>
<string name="error_maintenance_mode">El servidor está en modo mantenimiento</string>
<string name="error_400">400 Solicitud errónea</string>
<string name="error_401">401 no autorizado</string>
<string name="error_403">403 Prohibida</string>
<string name="error_404">404 no encontrado</string>
<string name="error_sync">Sincronización fallida: %1$s</string>
<string name="error_invalid_login">Inicio de sesión no válido: %1$s</string>
<string name="error_auth">Nombre de usuario o contraseña incorrectos</string>
<string name="error_json">Respuesta del servidor inválida</string>
<string name="error_req_failed">Petición fallida</string>
<string name="error_invalid_email">E-mail inválido</string>
<string name="error_invalid_project_id">ID de proyecto inválido</string>
<string name="error_invalid_project_name">Título del proyecto inválido</string>
<string name="error_invalid_bill_name">Nombre de factura no válido</string>
<string name="error_invalid_bill_date">Fecha de factura inválida</string>
<string name="error_invalid_bill_payer">Pagador requerido</string>
<string name="error_invalid_bill_owers">Propietarios requeridos</string>
<string name="error_no_network">No hay conexión de red</string>
<string name="error_server">Error del servidor</string>
<string name="error_io">Conexión con el servidor dañada</string>
<string name="error_share_impossible">No se puede compartir este proyecto</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Conectar a la cuenta de Nextcloud</string>
<string name="drawer_last_sync">Última sincronización: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancelar</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes"></string>
<string name="simple_no">Nu</string>
<string name="simple_close">Cerrar</string>
<!-- Settings -->
<string name="settings_appearance">Apariencia</string>
<string name="settings_network">Red</string>
<string name="settings_other">Otro</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Modo sin conexión</string>
<string name="settings_offline_mode_summary">Solo sincronizar manualmente.</string>
<string name="settings_color_custom">Color personalizado</string>
<string name="settings_color_mode">Selección de color</string>
<string name="settings_show_archived">Mostrar proyectos archivados</string>
<string name="settings_beta_features">Características beta</string>
<string name="settings_beta_features_summary">Activar características experimentales. Úsalo bajo tu propio riesgo.</string>
<string name="settings_url_warn_http">ADVERTENCIA: \"http\" no es seguro. Use \"https\".</string>
<string name="settings_colorpicker_title">Elegir color</string>
<string name="pref_value_color_system">Sistema</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Claro</string>
<string name="pref_value_theme_dark">Oscuro</string>
<string name="pref_value_theme_system">Seguir sistema</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repetir</string>
<string name="repeat_day">Diario</string>
<string name="repeat_week">Semanal</string>
<string name="repeat_fortnight">Fortnocturno</string>
<string name="repeat_month">Mensual</string>
<string name="repeat_year">Anual</string>
<string name="payment_mode_none">Ninguna</string>
<string name="payment_mode_all">Todos</string>
<string name="payment_mode_credit_card">Tarjeta de crédito</string>
<string name="payment_mode_cash">Dinero</string>
<string name="payment_mode_check">Comprobar</string>
<string name="payment_mode_online">En línea</string>
<string name="payment_mode_transfer">Transferir</string>
<string name="category_none">Ninguna</string>
<string name="category_all">Todos</string>
<string name="category_all_except_reimbursement">Todos excepto reembolso</string>
<string name="category_groceries">Comestible</string>
<string name="category_leisure">Bar/Fiesta</string>
<string name="category_rent">Alquiler</string>
<string name="category_bills">Factura</string>
<string name="category_excursion">Excursión/Cultura</string>
<string name="category_health">Salud</string>
<string name="category_shopping">Compras</string>
<string name="category_reimbursement">Reembolso</string>
<string name="category_restaurant">Restaurante</string>
<string name="category_accomodation">Alojamiento</string>
<string name="category_transport">Transporte</string>
<string name="category_sport">Deporte</string>
<!-- Project specific -->
<string name="new_project_action">Qué</string>
<string name="new_project_where">Donde</string>
<string name="where_local">Solo local</string>
<string name="where_cospend">Gastar</string>
<string name="where_ihatemoney">Dinero IHate</string>
<string name="todo_join">Unirse al proyecto existente</string>
<string name="todo_create">Crear nuevo proyecto</string>
<string name="import_tooltip">Importar desde archivo</string>
<string name="choose_account_project_dialog_title">Elegir proyecto</string>
<string name="choose_account_project_dialog_impossible">No se encontraron proyectos en esta cuenta.</string>
<string name="choose_project_management_action">Projekt</string>
<string name="project_added_success">Proyecto añadido correctamente.</string>
<string name="no_projects_text">Aún no tienes proyectos.</string>
<string name="configure_account_choice">Configurar cuenta Nextcloud</string>
<string name="add_project_choice">Añadir proyecto manualmente</string>
<string name="no_members_text">No hay miembros en este proyecto.</string>
<string name="no_bills_text">No se encontraron facturas.</string>
<string name="member_already_exists">El miembro ya existe.</string>
<string name="activity_dialog_title">Proyecto: %1$s</string>
<string name="remove_project_confirmation">Proyecto %1$s eliminado.</string>
<string name="file_saved_success">Archivo guardado: %1$s</string>
<string name="import_error_header">Error al importar en la fila %d</string>
<string name="import_error_date">Formato de fecha no válido en la fila %d</string>
<string name="import_error_owers">Dueños no válidos en la fila %d</string>
<string name="add_member_dialog_title">Añadir miembro</string>
<string name="edit_member_dialog_title">Editar miembro</string>
<string name="member_edit_delete">Eliminar</string>
<string name="project_edition_no_change">No hay cambios para guardar.</string>
<!-- Settlement -->
<string name="center_none">Ninguno (óptimo)</string>
<string name="settle_who">Quién paga</string>
<string name="settle_to_whom">A quien</string>
<string name="settle_how_much">Cantidad</string>
<string name="simple_settle_share">Compartir</string>
<string name="simple_create_bills">Crear facturas</string>
<string name="settle_bill_what">Acuerdo</string>
<!-- Currencies -->
<string name="currency_dialog_title">Elija la moneda (%s)</string>
<string name="setting_none">Ninguna</string>
<string name="setting_all">Todos</string>
<string name="currency_saved_success">Ajustes de moneda guardados.</string>
<string name="main_currency">Moneda principal</string>
<!-- Statistics -->
<string name="label_bills_suggested">Categorías sugeridas</string>
<string name="label_bills_skip">Saltar</string>
<string name="stats_date_min">De</string>
<string name="stats_date_max">A</string>
<string name="stats_who">Miembro</string>
<string name="stats_paid">Pagado</string>
<string name="stats_spent">Escrito</string>
<string name="stats_balance">Saldo</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Conexión fallida: %1$s</string>
<string name="error_create_remote_project_helper">Creación fallida: %1$s</string>
<string name="error_edit_remote_project_helper">Error al actualizar proyecto remoto: %1$s</string>
<string name="remote_project_operation_no_network">Red no disponible para operaciones remotas.</string>
<string name="error_scanning_bill_qr_code">Error al analizar el código QR.</string>
<string name="error_token_mismatch">El token de autenticación no coincide. Por favor, inicia sesión de nuevo.</string>
<string name="insufficient_access_level">No tienes permiso para realizar esta acción.</string>
<string name="delete_label_confirmation_title">Eliminar Etiqueta</string>
<string name="delete_label_confirmation_message">¿Está seguro que desea eliminar esta etiqueta?</string>
<!-- About -->
<string name="about_version">Versión %1$s</string>
<string name="about_maintainer_title">Mantenedor</string>
<string name="about_license_title">Licencia</string>
<string name="about_source_title">Código fuente</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Proyecto %1$s</string>
<string name="share_chooser_title">Compartir %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Lehmä</string>
<!-- Actions -->
<string name="action_new_bill">Uusi lasku</string>
<string name="action_add_project">Lisää projekti</string>
<string name="action_save">Tallenna</string>
<string name="action_edit">Muokkaa</string>
<string name="action_share">Jaa</string>
<string name="action_search">Etsi</string>
<string name="action_delete">Poista</string>
<string name="action_archive">Arkistoi</string>
<string name="action_unarchive">Poista</string>
<string name="action_export">Vie</string>
<string name="action_stats">Tilastot</string>
<string name="action_settle">Ratkaise</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Asetukset</string>
<string name="action_label_bills">Tunnisteet puuttuvat kategoriat</string>
<string name="action_logout">Kirjaudu Ulos</string>
<string name="action_connect">Yhdistä</string>
<string name="action_discard">Hylkää</string>
<string name="action_members">Jäsenet</string>
<string name="action_labels">Tunnisteet</string>
<string name="action_currencies">Valuutat</string>
<!-- Titles -->
<string name="title_stats">Tilastot</string>
<string name="title_edit_project">Muokkaa projektia</string>
<string name="title_label_bills">Merkitse Laskut</string>
<string name="title_labels">Hallitse Tunnisteita</string>
<string name="title_about">Tietoja</string>
<string name="title_settle">Ratkaise Projekti</string>
<string name="title_share">Jaa Projekti</string>
<string name="title_add_project">Lisää Projekti</string>
<string name="title_add_category">Lisää Kategoria</string>
<string name="title_add_payment_mode">Lisää Maksutila</string>
<string name="title_account">Nextcloud Tili</string>
<string name="title_share_web">Linkki</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Oletko varma?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Kaikki laskut</string>
<string name="label_categories">Kategoriat</string>
<string name="label_payment_modes">Maksutavat</string>
<string name="label_name">Nimi</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Väri</string>
<string name="label_weight">Paino</string>
<string name="label_activated">Aktivoitu</string>
<string name="label_password">Salasana</string>
<string name="label_email">Sähköposti</string>
<string name="label_url">Palvelimen osoite</string>
<string name="label_username">Käyttäjätunnus</string>
<string name="label_comment">Kommentti</string>
<string name="label_what">Mitä?</string>
<string name="label_payer">Kuka maksoi?</string>
<string name="label_owers">Kenen puolesta?</string>
<string name="label_repeat">Toista joka</string>
<string name="label_mode">Tila</string>
<string name="label_category">Kategoria</string>
<string name="label_project_id">Projektin ID/nimi</string>
<string name="label_project_title">Projektin otsikko</string>
<string name="label_use_sso">Käytä Nextcloud -sovellustiliä</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Tallentamattomat muutokset</string>
<string name="dialog_unsaved_changes_msg">Tallennetaanko muutokset ennen lähtöä?</string>
<string name="dialog_confirm_remove_project_msg">Etäprojektia ei poisteta.</string>
<string name="dialog_sync_error_title">Virhe synkronoinnissa</string>
<string name="dialog_sync_error_msg">%1$ssynkronointi epäonnistui.\n\n%2$s</string>
<string name="dialog_balanced_msg">Menot ovat jo tasapainossa.</string>
<string name="msg_project_added">Projekti %1$s lisätty</string>
<string name="msg_bill_labeled_done">Kaikki laskut merkitty</string>
<string name="msg_no_suggestions">Ei ehdotuksia</string>
<string name="msg_auth_warning">Tarvitsee Kustannukset v0.3.4+.</string>
<string name="msg_link_copied">Linkki kopioitu leikepöydälle</string>
<string name="msg_share_qr">Skannaa QR-koodi tai jaa linkki johon liittyä.</string>
<string name="msg_share_web">Linkki selaimen käyttöön.</string>
<string name="msg_share_qr_warn">Jaa tämä linkki Cowkäytetyn käyttäjän kanssa.</string>
<string name="msg_settle_intro">%1$s:n toimitus:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Tilastot %1$s:</string>
<string name="msg_stats_header">Jäsenen (Käytetty Saldo)</string>
<string name="msg_logged_in_as">Kirjautunut sisään nimellä %1$s</string>
<!-- Errors -->
<string name="error_generic">Virhe</string>
<string name="error_loading">Ladataan</string>
<string name="error_no_projects">Projekteja ei löytynyt</string>
<string name="error_no_members">Jäseniä ei löytynyt</string>
<string name="error_no_bills">Laskuja ei löytynyt</string>
<string name="error_no_member">Vaadittu vähintään yksi jäsen</string>
<string name="error_maintenance_mode">Palvelin on huoltotilassa</string>
<string name="error_400">400 Virheellinen pyyntö</string>
<string name="error_401">401 Luvaton</string>
<string name="error_403">403 Kielletty</string>
<string name="error_404">404 Ei Löytynyt</string>
<string name="error_sync">Synkronointi epäonnistui: %1$s</string>
<string name="error_invalid_login">Virheellinen kirjautuminen: %1$s</string>
<string name="error_auth">Väärä käyttäjätunnus tai salasana</string>
<string name="error_json">Virheellinen palvelimen vastaus</string>
<string name="error_req_failed">Pyyntö epäonnistui</string>
<string name="error_invalid_email">Virheellinen sähköpostiosoite</string>
<string name="error_invalid_project_id">Virheellinen projektin tunnus</string>
<string name="error_invalid_project_name">Virheellinen projektin otsikko</string>
<string name="error_invalid_bill_name">Virheellinen laskun nimi</string>
<string name="error_invalid_bill_date">Virheellinen laskun päivämäärä</string>
<string name="error_invalid_bill_payer">Maksaja vaaditaan</string>
<string name="error_invalid_bill_owers">Uutiskirjeet vaaditaan</string>
<string name="error_no_network">Ei verkkoyhteyttä</string>
<string name="error_server">Palvelin virhe</string>
<string name="error_io">Palvelinyhteys rikki</string>
<string name="error_share_impossible">Projektia ei voi jakaa</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Yhdistä Nextcloud tiliin</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Peruuta</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">Kyllä</string>
<string name="simple_no">Ei</string>
<string name="simple_close">Sulje</string>
<!-- Settings -->
<string name="settings_appearance">Ulkoasu</string>
<string name="settings_network">Verkko</string>
<string name="settings_other">Muu</string>
<string name="settings_night_mode">Teema</string>
<string name="settings_offline_mode">Offline-tila</string>
<string name="settings_offline_mode_summary">Synkronoi vain käsin.</string>
<string name="settings_color_custom">Mukautettu väri</string>
<string name="settings_color_mode">Värin Valinta</string>
<string name="settings_show_archived">Näytä arkistoidut projektit</string>
<string name="settings_beta_features">Beta Ominaisuudet</string>
<string name="settings_beta_features_summary">Ota käyttöön kokeelliset ominaisuudet. Käytä omalla vastuullasi.</string>
<string name="settings_url_warn_http">VAROITUS: \"http\" ei ole turvallinen. Käytä \"℃\".</string>
<string name="settings_colorpicker_title">Valitse Väri</string>
<string name="pref_value_color_system">Järjestelmä</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manuaalinen</string>
<string name="pref_value_theme_light">Vaalea</string>
<string name="pref_value_theme_dark">Tumma</string>
<string name="pref_value_theme_system">Seuraa järjestelmää</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Ei toistumista</string>
<string name="repeat_day">Päivittäin</string>
<string name="repeat_week">Viikoittain</string>
<string name="repeat_fortnight">Onneksi</string>
<string name="repeat_month">Kuukausittain</string>
<string name="repeat_year">Vuosittain</string>
<string name="payment_mode_none">Ei Mitään</string>
<string name="payment_mode_all">Kaikki</string>
<string name="payment_mode_credit_card">Luottokortti</string>
<string name="payment_mode_cash">Käteinen</string>
<string name="payment_mode_check">Tarkista</string>
<string name="payment_mode_online">Paikalla</string>
<string name="payment_mode_transfer">Siirto</string>
<string name="category_none">Ei Mitään</string>
<string name="category_all">Kaikki</string>
<string name="category_all_except_reimbursement">Kaikki paitsi korvaukset</string>
<string name="category_groceries">Ruokakauppa</string>
<string name="category_leisure">Baari/Juhla</string>
<string name="category_rent">Vuokra</string>
<string name="category_bills">Lasku</string>
<string name="category_excursion">Retki/Kulttuuri</string>
<string name="category_health">Terveys</string>
<string name="category_shopping">Ostokset</string>
<string name="category_reimbursement">Korvaus</string>
<string name="category_restaurant">Ravintola</string>
<string name="category_accomodation">Majoitus</string>
<string name="category_transport">Kuljetus</string>
<string name="category_sport">Urheilu</string>
<!-- Project specific -->
<string name="new_project_action">Mitä</string>
<string name="new_project_where">Missä</string>
<string name="where_local">Vain paikallinen</string>
<string name="where_cospend">Kulut</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Liity olemassa olevaan projektiin</string>
<string name="todo_create">Luo uusi projekti</string>
<string name="import_tooltip">Tuo tiedostosta</string>
<string name="choose_account_project_dialog_title">Valitse projekti</string>
<string name="choose_account_project_dialog_impossible">Tällä tilillä ei löytynyt projekteja.</string>
<string name="choose_project_management_action">Projekti</string>
<string name="project_added_success">Projekti lisätty onnistuneesti.</string>
<string name="no_projects_text">Sinulla ei ole vielä projekteja.</string>
<string name="configure_account_choice">Määritä Nextcloud tili</string>
<string name="add_project_choice">Lisää projekti käsin</string>
<string name="no_members_text">Ei jäseniä tässä projektissa.</string>
<string name="no_bills_text">Laskuja ei löytynyt.</string>
<string name="member_already_exists">Jäsentä on jo olemassa.</string>
<string name="activity_dialog_title">Projekti: %1$s</string>
<string name="remove_project_confirmation">Projekti %1$s poistettu.</string>
<string name="file_saved_success">Tiedosto tallennettu: %1$s</string>
<string name="import_error_header">Tuonti epäonnistui rivillä %d</string>
<string name="import_error_date">Päivämäärän muoto ei kelpaa rivillä %d</string>
<string name="import_error_owers">Virheelliset ruohonleikkurit rivillä %d</string>
<string name="add_member_dialog_title">Lisää Jäsen</string>
<string name="edit_member_dialog_title">Muokkaa Jäsen</string>
<string name="member_edit_delete">Poista</string>
<string name="project_edition_no_change">Tallennettavia muutoksia ei ole.</string>
<!-- Settlement -->
<string name="center_none">Ei Mitään (valinnainen)</string>
<string name="settle_who">Kuka maksaa</string>
<string name="settle_to_whom">Kenelle</string>
<string name="settle_how_much">Määrä</string>
<string name="simple_settle_share">Jaa</string>
<string name="simple_create_bills">Luo laskuja</string>
<string name="settle_bill_what">Selvitys</string>
<!-- Currencies -->
<string name="currency_dialog_title">Valitse Valuutta (%s)</string>
<string name="setting_none">Ei Mitään</string>
<string name="setting_all">Kaikki</string>
<string name="currency_saved_success">Valuutan asetukset tallennettu.</string>
<string name="main_currency">Päävaluutta</string>
<!-- Statistics -->
<string name="label_bills_suggested">Ehdotetut Kategoriat</string>
<string name="label_bills_skip">Ohita</string>
<string name="stats_date_min">Alkaen</string>
<string name="stats_date_max">Vastaanottaja</string>
<string name="stats_who">Jäsen</string>
<string name="stats_paid">Maksettu</string>
<string name="stats_spent">Käytetty</string>
<string name="stats_balance">Saldo</string>
<string name="total">Yhteensä: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Yhteys epäonnistui: %1$s</string>
<string name="error_create_remote_project_helper">Luonti epäonnistui: %1$s</string>
<string name="error_edit_remote_project_helper">Virhe päivitettäessä etäporjektia: %1$s</string>
<string name="remote_project_operation_no_network">Verkko ei ole käytettävissä etäkäyttöön.</string>
<string name="error_scanning_bill_qr_code">QR-koodin jäsentäminen epäonnistui.</string>
<string name="error_token_mismatch">Todennustunnus ei täsmää. Kirjaudu sisään uudelleen.</string>
<string name="insufficient_access_level">Sinulla ei ole oikeuksia suorittaa tätä toimintoa.</string>
<string name="delete_label_confirmation_title">Poista Tunniste</string>
<string name="delete_label_confirmation_message">Oletko varma, että haluat poistaa tämän tunnisteen?</string>
<!-- About -->
<string name="about_version">Versio %1$s</string>
<string name="about_maintainer_title">Ylläpitäjä</string>
<string name="about_license_title">Lisenssi</string>
<string name="about_source_title">Lähdekoodi</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projekti %1$s</string>
<string name="share_chooser_title">Jaa %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Vache dépensée</string>
<!-- Actions -->
<string name="action_new_bill">Nouvelle facture</string>
<string name="action_add_project">Ajouter un projet</string>
<string name="action_save">Enregistrer</string>
<string name="action_edit">Editer</string>
<string name="action_share">Partager</string>
<string name="action_search">Chercher</string>
<string name="action_delete">Supprimez</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Désarchiver</string>
<string name="action_export">Exportation</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Régler</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Réglages</string>
<string name="action_label_bills">Catégories manquantes</string>
<string name="action_logout">Déconnexion</string>
<string name="action_connect">Connecter</string>
<string name="action_discard">Abandonner</string>
<string name="action_members">Membres</string>
<string name="action_labels">Étiquettes</string>
<string name="action_currencies">Devises</string>
<!-- Titles -->
<string name="title_stats">Statistiques</string>
<string name="title_edit_project">Modifier le projet</string>
<string name="title_label_bills">Étiquettes de factures</string>
<string name="title_labels">Gérer les étiquettes</string>
<string name="title_about">À propos de</string>
<string name="title_settle">Régler le projet</string>
<string name="title_share">Partager le projet</string>
<string name="title_add_project">Ajouter un projet</string>
<string name="title_add_category">Ajouter une catégorie</string>
<string name="title_add_payment_mode">Ajouter un mode de paiement</string>
<string name="title_account">Compte Nextcloud</string>
<string name="title_share_web">Lien web</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Êtes-vous sûr(e) ?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Toutes les factures</string>
<string name="label_categories">Catégories</string>
<string name="label_payment_modes">Modes de paiement</string>
<string name="label_name">Nom</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Couleur</string>
<string name="label_weight">Poids</string>
<string name="label_activated">Activé</string>
<string name="label_password">Mot de passe</string>
<string name="label_email">Courriel</string>
<string name="label_url">Adresse du serveur</string>
<string name="label_username">Nom d\'utilisateur</string>
<string name="label_comment">Commenter</string>
<string name="label_what">Quoi?</string>
<string name="label_payer">Qui a payé ?</string>
<string name="label_owers">Pour qui?</string>
<string name="label_repeat">Répéter toutes les</string>
<string name="label_mode">Mode</string>
<string name="label_category">Catégorie</string>
<string name="label_project_id">ID/nom du projet</string>
<string name="label_project_title">Titre du projet</string>
<string name="label_use_sso">Utiliser le compte Nextcloud App</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Modifications non enregistrées</string>
<string name="dialog_unsaved_changes_msg">Enregistrer les modifications avant de partir?</string>
<string name="dialog_confirm_remove_project_msg">Le projet distant ne sera pas supprimé.</string>
<string name="dialog_sync_error_title">Erreur de synchronisation</string>
<string name="dialog_sync_error_msg">Échec de la synchronisation pour %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Les dépenses sont déjà équilibrées.</string>
<string name="msg_project_added">Projet %1$s ajouté</string>
<string name="msg_bill_labeled_done">Toutes les factures étiquetées</string>
<string name="msg_no_suggestions">Aucune suggestion</string>
<string name="msg_auth_warning">Nécessite Cospend v0.3.4+.</string>
<string name="msg_link_copied">Lien copié dans le presse-papiers</string>
<string name="msg_share_qr">Scannez le code QR ou partagez le lien pour vous inscrire.</string>
<string name="msg_share_web">Lien pour accéder au navigateur Web.</string>
<string name="msg_share_qr_warn">Partagez ce lien avec un utilisateur Cowspent</string>
<string name="msg_settle_intro">Réglage pour %1$s:</string>
<string name="msg_settle_sentence">%1$s doit %3$.2f à %2$s</string>
<string name="msg_stats_intro">Statistiques pour %1$s:</string>
<string name="msg_stats_header">Membre (payé | Dépensé | Solde)</string>
<string name="msg_logged_in_as">Connecté en tant que %1$s</string>
<!-- Errors -->
<string name="error_generic">Erreur</string>
<string name="error_loading">En cours de chargement</string>
<string name="error_no_projects">Aucun projet trouvé</string>
<string name="error_no_members">Aucun membre trouvé</string>
<string name="error_no_bills">Aucune facture trouvée</string>
<string name="error_no_member">Au moins un membre est requis</string>
<string name="error_maintenance_mode">Le serveur est en mode maintenance</string>
<string name="error_400">400 Mauvaise requête</string>
<string name="error_401">401 Non autorisé</string>
<string name="error_403">403 Interdit</string>
<string name="error_404">404 introuvable</string>
<string name="error_sync">Échec de la synchronisation : %1$s</string>
<string name="error_invalid_login">Identifiant invalide : %1$s</string>
<string name="error_auth">Mauvais nom d\'utilisateur ou mot de passe</string>
<string name="error_json">Réponse du serveur invalide</string>
<string name="error_req_failed">La requête a échoué</string>
<string name="error_invalid_email">Adresse e-mail invalide</string>
<string name="error_invalid_project_id">ID de projet invalide</string>
<string name="error_invalid_project_name">Titre du projet non valide</string>
<string name="error_invalid_bill_name">Nom de facture invalide</string>
<string name="error_invalid_bill_date">Date de facture invalide</string>
<string name="error_invalid_bill_payer">Payeur requis</string>
<string name="error_invalid_bill_owers">Propriétaires requis</string>
<string name="error_no_network">Aucune connexion réseau</string>
<string name="error_server">Erreur serveur</string>
<string name="error_io">La connexion au serveur a échoué</string>
<string name="error_share_impossible">Impossible de partager ce projet</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Se connecter au compte Nextcloud</string>
<string name="drawer_last_sync">Dernière synchronisation : %1$02d:%2$02d</string>
<string name="simple_cancel">Abandonner</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Oui</string>
<string name="simple_no">Non</string>
<string name="simple_close">Fermer</string>
<!-- Settings -->
<string name="settings_appearance">Apparence</string>
<string name="settings_network">Réseau</string>
<string name="settings_other">Autres</string>
<string name="settings_night_mode">Thème</string>
<string name="settings_offline_mode">Mode hors-ligne</string>
<string name="settings_offline_mode_summary">Synchroniser uniquement manuellement.</string>
<string name="settings_color_custom">Couleur personnalisée</string>
<string name="settings_color_mode">Sélection de la couleur</string>
<string name="settings_show_archived">Afficher les projets archivés</string>
<string name="settings_beta_features">Fonctionnalités bêta</string>
<string name="settings_beta_features_summary">Activer les fonctionnalités expérimentales. Utiliser à vos propres risques.</string>
<string name="settings_url_warn_http">AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\".</string>
<string name="settings_colorpicker_title">Choisir une couleur</string>
<string name="pref_value_color_system">Système</string>
<string name="pref_value_color_server">Nuage suivant</string>
<string name="pref_value_color_manual">Manuelle</string>
<string name="pref_value_theme_light">Lumière</string>
<string name="pref_value_theme_dark">Sombre</string>
<string name="pref_value_theme_system">Suivre le système</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Pas de répétition</string>
<string name="repeat_day">Tous les jours</string>
<string name="repeat_week">Hebdomadaire</string>
<string name="repeat_fortnight">Tous les quinze jours</string>
<string name="repeat_month">Mensuel</string>
<string name="repeat_year">Annuel</string>
<string name="payment_mode_none">Aucun</string>
<string name="payment_mode_all">Tous</string>
<string name="payment_mode_credit_card">Carte de crédit</string>
<string name="payment_mode_cash">Espèces</string>
<string name="payment_mode_check">Contrôler</string>
<string name="payment_mode_online">En ligne</string>
<string name="payment_mode_transfer">Transférer</string>
<string name="category_none">Aucun</string>
<string name="category_all">Tous</string>
<string name="category_all_except_reimbursement">Tout sauf remboursement</string>
<string name="category_groceries">Épicerie</string>
<string name="category_leisure">Barre/Fête</string>
<string name="category_rent">Louer</string>
<string name="category_bills">Facture</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Santé</string>
<string name="category_shopping">Achats</string>
<string name="category_reimbursement">Remboursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Hébergement</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Qu\'est-ce que</string>
<string name="new_project_where">Où se trouve</string>
<string name="where_local">Seulement en local</string>
<string name="where_cospend">Codépense</string>
<string name="where_ihatemoney">format@@0 IHateMoney</string>
<string name="todo_join">Rejoindre un projet existant</string>
<string name="todo_create">Créer un nouveau projet</string>
<string name="import_tooltip">Importer depuis un fichier</string>
<string name="choose_account_project_dialog_title">Choisir un projet</string>
<string name="choose_account_project_dialog_impossible">Aucun projet trouvé sur ce compte.</string>
<string name="choose_project_management_action">Projet</string>
<string name="project_added_success">Le projet a été ajouté avec succès.</string>
<string name="no_projects_text">Vous n\'avez pas encore de projets.</string>
<string name="configure_account_choice">Configurer le compte Nextcloud</string>
<string name="add_project_choice">Ajouter un projet manuellement</string>
<string name="no_members_text">Aucun membre dans ce projet.</string>
<string name="no_bills_text">Aucune facture trouvée.</string>
<string name="member_already_exists">Ce membre existe déjà.</string>
<string name="activity_dialog_title">Projet: %1$s</string>
<string name="remove_project_confirmation">Projet %1$s supprimé.</string>
<string name="file_saved_success">Fichier enregistré : %1$s</string>
<string name="import_error_header">Échec de l\'importation à la ligne %d</string>
<string name="import_error_date">Format de date invalide à la ligne %d</string>
<string name="import_error_owers">Propriétés non valides à la ligne %d</string>
<string name="add_member_dialog_title">Ajouter un membre</string>
<string name="edit_member_dialog_title">Modifier le membre</string>
<string name="member_edit_delete">Supprimez</string>
<string name="project_edition_no_change">Aucune modification à enregistrer.</string>
<!-- Settlement -->
<string name="center_none">Aucun (optimiste)</string>
<string name="settle_who">Qui paie</string>
<string name="settle_to_whom">À qui</string>
<string name="settle_how_much">Montant</string>
<string name="simple_settle_share">Partager</string>
<string name="simple_create_bills">Créer des factures</string>
<string name="settle_bill_what">Réglement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choisir la devise (%s)</string>
<string name="setting_none">Aucun</string>
<string name="setting_all">Tous</string>
<string name="currency_saved_success">Paramètres des devises enregistrés.</string>
<string name="main_currency">Devise principale</string>
<!-- Statistics -->
<string name="label_bills_suggested">Catégories suggérées</string>
<string name="label_bills_skip">Ignorer</string>
<string name="stats_date_min">A partir de</string>
<string name="stats_date_max">À</string>
<string name="stats_who">Membre</string>
<string name="stats_paid">Payé</string>
<string name="stats_spent">Dépensé</string>
<string name="stats_balance">Solde</string>
<string name="total">Total : %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Échec de la connexion : %1$s</string>
<string name="error_create_remote_project_helper">Échec de la création : %1$s</string>
<string name="error_edit_remote_project_helper">Erreur lors de la mise à jour du projet distant : %1$s</string>
<string name="remote_project_operation_no_network">Réseau indisponible pour le fonctionnement à distance.</string>
<string name="error_scanning_bill_qr_code">Impossible d\'analyser le code QR.</string>
<string name="error_token_mismatch">Incompatibilité du jeton d\'authentification. Veuillez vous reconnecter.</string>
<string name="insufficient_access_level">Vous n\'avez pas la permission d\'effectuer cette action.</string>
<string name="delete_label_confirmation_title">Supprimer l\'étiquette</string>
<string name="delete_label_confirmation_message">Êtes-vous sûr de vouloir supprimer ce label ?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Mainteneur</string>
<string name="about_license_title">Licence</string>
<string name="about_source_title">Code source</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projet %1$s</string>
<string name="share_chooser_title">Partager %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">New bill</string>
<string name="action_add_project">Add project</string>
<string name="action_save">Save</string>
<string name="action_edit">Edit</string>
<string name="action_share">Share</string>
<string name="action_search">Search</string>
<string name="action_delete">Delete</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string>
<string name="action_export">Export</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Settle</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Settings</string>
<string name="action_label_bills">Label missing categories</string>
<string name="action_logout">Logout</string>
<string name="action_connect">Connect</string>
<string name="action_discard">Discard</string>
<string name="action_members">Members</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Currencies</string>
<!-- Titles -->
<string name="title_stats">Statistics</string>
<string name="title_edit_project">Edit project</string>
<string name="title_label_bills">Label Bills</string>
<string name="title_labels">Manage Labels</string>
<string name="title_about">About</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Are you sure?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">All bills</string>
<string name="label_categories">Categories</string>
<string name="label_payment_modes">Payment Modes</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Weight</string>
<string name="label_activated">Activated</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server address</string>
<string name="label_username">Username</string>
<string name="label_comment">Comment</string>
<string name="label_what">What?</string>
<string name="label_payer">Who paid?</string>
<string name="label_owers">For whom?</string>
<string name="label_repeat">Repeat every</string>
<string name="label_mode">Mode</string>
<string name="label_category">Category</string>
<string name="label_project_id">Project ID/name</string>
<string name="label_project_title">Project title</string>
<string name="label_use_sso">Use Nextcloud App Account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
<string name="dialog_sync_error_title">Sync error</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">All bills labeled</string>
<string name="msg_no_suggestions">No suggestions</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
<string name="msg_share_web">Link for web browser access.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Stats for %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Loading</string>
<string name="error_no_projects">No projects found</string>
<string name="error_no_members">No members found</string>
<string name="error_no_bills">No bills found</string>
<string name="error_no_member">At least one member required</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_400">400 Bad request</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 Forbidden</string>
<string name="error_404">404 Not Found</string>
<string name="error_sync">Sync failed: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string>
<string name="error_auth">Wrong username or password</string>
<string name="error_json">Invalid server response</string>
<string name="error_req_failed">Request failed</string>
<string name="error_invalid_email">Invalid e-mail</string>
<string name="error_invalid_project_id">Invalid project ID</string>
<string name="error_invalid_project_name">Invalid project title</string>
<string name="error_invalid_bill_name">Invalid bill name</string>
<string name="error_invalid_bill_date">Invalid bill date</string>
<string name="error_invalid_bill_payer">Payer required</string>
<string name="error_invalid_bill_owers">Owers required</string>
<string name="error_no_network">No network connection</string>
<string name="error_server">Server error</string>
<string name="error_io">Server connection broken</string>
<string name="error_share_impossible">Cannot share this project</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Yes</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<!-- Settings -->
<string name="settings_appearance">Appearance</string>
<string name="settings_network">Network</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: \"http\" is unsafe. Use \"https\".</string>
<string name="settings_colorpicker_title">Choose Color</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repeat</string>
<string name="repeat_day">Daily</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Monthly</string>
<string name="repeat_year">Yearly</string>
<string name="payment_mode_none">None</string>
<string name="payment_mode_all">All</string>
<string name="payment_mode_credit_card">Credit card</string>
<string name="payment_mode_cash">Cash</string>
<string name="payment_mode_check">Check</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">None</string>
<string name="category_all">All</string>
<string name="category_all_except_reimbursement">All except reimbursement</string>
<string name="category_groceries">Grocery</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Rent</string>
<string name="category_bills">Bill</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Health</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Reimbursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodation</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">Where</string>
<string name="where_local">Local only</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Join existing project</string>
<string name="todo_create">Create new project</string>
<string name="import_tooltip">Import from file</string>
<string name="choose_account_project_dialog_title">Choose project</string>
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project added successfully.</string>
<string name="no_projects_text">You have no projects yet.</string>
<string name="configure_account_choice">Configure Nextcloud account</string>
<string name="add_project_choice">Add project manually</string>
<string name="no_members_text">No members in this project.</string>
<string name="no_bills_text">No bills found.</string>
<string name="member_already_exists">Member already exists.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File saved: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Invalid date format at row %d</string>
<string name="import_error_owers">Invalid owers at row %d</string>
<string name="add_member_dialog_title">Add Member</string>
<string name="edit_member_dialog_title">Edit Member</string>
<string name="member_edit_delete">Delete</string>
<string name="project_edition_no_change">No changes to save.</string>
<!-- Settlement -->
<string name="center_none">None (Optimal)</string>
<string name="settle_who">Who pays</string>
<string name="settle_to_whom">To whom</string>
<string name="settle_how_much">Amount</string>
<string name="simple_settle_share">Share</string>
<string name="simple_create_bills">Create bills</string>
<string name="settle_bill_what">Settlement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string>
<string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
<string name="main_currency">Main Currency</string>
<!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string>
<string name="label_bills_skip">Skip</string>
<string name="stats_date_min">From</string>
<string name="stats_date_max">To</string>
<string name="stats_who">Member</string>
<string name="stats_paid">Paid</string>
<string name="stats_spent">Spent</string>
<string name="stats_balance">Balance</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
<string name="delete_label_confirmation_title">Delete Label</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Maintainer</string>
<string name="about_license_title">License</string>
<string name="about_source_title">Source code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">Nuova bolletta</string>
<string name="action_add_project">Aggiungi progetto</string>
<string name="action_save">Salva</string>
<string name="action_edit">Modifica</string>
<string name="action_share">Condividi</string>
<string name="action_search">Cerca</string>
<string name="action_delete">Elimina</string>
<string name="action_archive">Archivio</string>
<string name="action_unarchive">Disarchivia</string>
<string name="action_export">Esporta</string>
<string name="action_stats">Statistiche</string>
<string name="action_settle">Salta</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Impostazioni</string>
<string name="action_label_bills">Etichetta categorie mancanti</string>
<string name="action_logout">Esci</string>
<string name="action_connect">Connetti</string>
<string name="action_discard">Scarta</string>
<string name="action_members">Membri</string>
<string name="action_labels">Etichette</string>
<string name="action_currencies">Valute</string>
<!-- Titles -->
<string name="title_stats">Statistiche</string>
<string name="title_edit_project">Modifica progetto</string>
<string name="title_label_bills">Etichetta Di Bollette</string>
<string name="title_labels">Gestisci Etichette</string>
<string name="title_about">Informazioni</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Condividi Progetto</string>
<string name="title_add_project">Aggiungi Progetto</string>
<string name="title_add_category">Aggiungi Categoria</string>
<string name="title_add_payment_mode">Aggiungi Modalità Di Pagamento</string>
<string name="title_account">Account Nextcloud</string>
<string name="title_share_web">Collegamento web</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Sei sicuro?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Tutte le bollette</string>
<string name="label_categories">Categorie</string>
<string name="label_payment_modes">Modalità Di Pagamento</string>
<string name="label_name">Nome</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Colore</string>
<string name="label_weight">Peso</string>
<string name="label_activated">Attivato</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Indirizzo del server</string>
<string name="label_username">Username</string>
<string name="label_comment">Commento</string>
<string name="label_what">Cosa?</string>
<string name="label_payer">Chi ha pagato?</string>
<string name="label_owers">Per chi?</string>
<string name="label_repeat">Ripeti ogni</string>
<string name="label_mode">Modalità</string>
<string name="label_category">Categoria</string>
<string name="label_project_id">Id Del Progetto/Nome</string>
<string name="label_project_title">Titolo del progetto</string>
<string name="label_use_sso">Usa Account App Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Modifiche non salvate</string>
<string name="dialog_unsaved_changes_msg">Salvare le modifiche prima di partire?</string>
<string name="dialog_confirm_remove_project_msg">Il progetto remoto non verrà eliminato.</string>
<string name="dialog_sync_error_title">Errore di sincronizzazione</string>
<string name="dialog_sync_error_msg">Sincronizzazione fallita per %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Le spese sono già bilanciate.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">Tutte le fatture etichettate</string>
<string name="msg_no_suggestions">Nessun suggerimento</string>
<string name="msg_auth_warning">Richiede Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copiato negli appunti</string>
<string name="msg_share_qr">Scansiona il codice QR o condividi il link per unirsi.</string>
<string name="msg_share_web">Link per l\'accesso al browser web.</string>
<string name="msg_share_qr_warn">Condividi questo link con un utente Cowspent</string>
<string name="msg_settle_intro">Regolamento per %1$s:</string>
<string name="msg_settle_sentence">%1$s deve %3$.2f a %2$s</string>
<string name="msg_stats_intro">Statistiche per %1$s:</string>
<string name="msg_stats_header">Membro (Saldo &lt;unk&gt; Speso &lt;unk&gt; Balance)</string>
<string name="msg_logged_in_as">Accesso effettuato come %1$s</string>
<!-- Errors -->
<string name="error_generic">Errore</string>
<string name="error_loading">Caricamento</string>
<string name="error_no_projects">Nessun progetto trovato</string>
<string name="error_no_members">Nessun membro trovato</string>
<string name="error_no_bills">Nessuna bolletta trovata</string>
<string name="error_no_member">Almeno un membro richiesto</string>
<string name="error_maintenance_mode">Il server è in modalità manutenzione</string>
<string name="error_400">400 Richiesta errata</string>
<string name="error_401">401 Non Autorizzato</string>
<string name="error_403">403 Vietato</string>
<string name="error_404">404 Non Trovato</string>
<string name="error_sync">Sincronizzazione non riuscita: %1$s</string>
<string name="error_invalid_login">Login non valido: %1$s</string>
<string name="error_auth">Nome utente o password errati</string>
<string name="error_json">Risposta server non valida</string>
<string name="error_req_failed">Richiesta fallita</string>
<string name="error_invalid_email">E-mail non valida</string>
<string name="error_invalid_project_id">ID progetto non valido</string>
<string name="error_invalid_project_name">Titolo progetto non valido</string>
<string name="error_invalid_bill_name">Nome fattura non valido</string>
<string name="error_invalid_bill_date">Data fattura non valida</string>
<string name="error_invalid_bill_payer">Pagatore richiesto</string>
<string name="error_invalid_bill_owers">Proprietari richiesti</string>
<string name="error_no_network">Nessuna connessione di rete</string>
<string name="error_server">Errore del server</string>
<string name="error_io">Connessione al server interrotta</string>
<string name="error_share_impossible">Impossibile condividere questo progetto</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connetti all\'account Nextcloud</string>
<string name="drawer_last_sync">Ultima sincronizzazione: %1$02d:%2$02d</string>
<string name="simple_cancel">Annulla</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes"></string>
<string name="simple_no">No</string>
<string name="simple_close">Chiudi</string>
<!-- Settings -->
<string name="settings_appearance">Aspetto</string>
<string name="settings_network">Rete</string>
<string name="settings_other">Altro</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Modalità offline</string>
<string name="settings_offline_mode_summary">Sincronizza solo manualmente.</string>
<string name="settings_color_custom">Colore personalizzato</string>
<string name="settings_color_mode">Selezione Colore</string>
<string name="settings_show_archived">Mostra progetti archiviati</string>
<string name="settings_beta_features">Caratteristiche Beta</string>
<string name="settings_beta_features_summary">Abilita le funzioni sperimentali. Usa a tuo rischio.</string>
<string name="settings_url_warn_http">ATTENZIONE: \"http\" non è sicuro. Usa \"국\".</string>
<string name="settings_colorpicker_title">Scegli Colore</string>
<string name="pref_value_color_system">Sistema</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manuale</string>
<string name="pref_value_theme_light">Chiaro</string>
<string name="pref_value_theme_dark">Scuro</string>
<string name="pref_value_theme_system">Segui sistema</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Nessuna ripetizione</string>
<string name="repeat_day">Giornaliero</string>
<string name="repeat_week">Settimanale</string>
<string name="repeat_fortnight">Quindici</string>
<string name="repeat_month">Mensile</string>
<string name="repeat_year">Annuale</string>
<string name="payment_mode_none">Nessuno</string>
<string name="payment_mode_all">Tutti</string>
<string name="payment_mode_credit_card">Carta di credito</string>
<string name="payment_mode_cash">Contanti</string>
<string name="payment_mode_check">Controlla</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Trasferisci</string>
<string name="category_none">Nessuno</string>
<string name="category_all">Tutti</string>
<string name="category_all_except_reimbursement">Tutti tranne il rimborso</string>
<string name="category_groceries">Alimentari</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Affitto</string>
<string name="category_bills">Fattura</string>
<string name="category_excursion">Escursione/Cultura</string>
<string name="category_health">Salute</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Rimborso</string>
<string name="category_restaurant">Ristorante</string>
<string name="category_accomodation">Alloggio</string>
<string name="category_transport">Trasporti</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Cosa</string>
<string name="new_project_where">Dove</string>
<string name="where_local">Solo locale</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Unisciti al progetto esistente</string>
<string name="todo_create">Crea nuovo progetto</string>
<string name="import_tooltip">Importa da file</string>
<string name="choose_account_project_dialog_title">Scegli progetto</string>
<string name="choose_account_project_dialog_impossible">Nessun progetto trovato su questo account.</string>
<string name="choose_project_management_action">Progetto</string>
<string name="project_added_success">Progetto aggiunto con successo.</string>
<string name="no_projects_text">Non hai ancora alcun progetto.</string>
<string name="configure_account_choice">Configura account Nextcloud</string>
<string name="add_project_choice">Aggiunge manualmente il progetto</string>
<string name="no_members_text">Nessun membro in questo progetto.</string>
<string name="no_bills_text">Nessuna bolletta trovata.</string>
<string name="member_already_exists">Membro già esistente.</string>
<string name="activity_dialog_title">Progetto: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File salvato: %1$s</string>
<string name="import_error_header">Importazione fallita alla riga %d</string>
<string name="import_error_date">Formato data non valido alla riga %d</string>
<string name="import_error_owers">Guanti non validi alla riga %d</string>
<string name="add_member_dialog_title">Aggiungi Membro</string>
<string name="edit_member_dialog_title">Modifica Membro</string>
<string name="member_edit_delete">Elimina</string>
<string name="project_edition_no_change">Nessuna modifica da salvare.</string>
<!-- Settlement -->
<string name="center_none">Nessuno (ottimale)</string>
<string name="settle_who">Chi paga</string>
<string name="settle_to_whom">A chi</string>
<string name="settle_how_much">Importo</string>
<string name="simple_settle_share">Condividi</string>
<string name="simple_create_bills">Crea bollette</string>
<string name="settle_bill_what">Regolamento</string>
<!-- Currencies -->
<string name="currency_dialog_title">Scegli Valuta (%s)</string>
<string name="setting_none">Nessuno</string>
<string name="setting_all">Tutti</string>
<string name="currency_saved_success">Impostazioni valuta salvate.</string>
<string name="main_currency">Valuta Principale</string>
<!-- Statistics -->
<string name="label_bills_suggested">Categorie Suggerite</string>
<string name="label_bills_skip">Salta</string>
<string name="stats_date_min">Da</string>
<string name="stats_date_max">A</string>
<string name="stats_who">Membro</string>
<string name="stats_paid">Pagato</string>
<string name="stats_spent">Speso</string>
<string name="stats_balance">Saldo</string>
<string name="total">Totale: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connessione non riuscita: %1$s</string>
<string name="error_create_remote_project_helper">Creazione fallita: %1$s</string>
<string name="error_edit_remote_project_helper">Errore nell\'aggiornare il progetto remoto: %1$s</string>
<string name="remote_project_operation_no_network">Rete non disponibile per operazioni remote.</string>
<string name="error_scanning_bill_qr_code">Analisi del codice QR non riuscita.</string>
<string name="error_token_mismatch">Token di autenticazione non corrisponde. Effettua nuovamente il login.</string>
<string name="insufficient_access_level">Non hai i permessi per eseguire questa azione.</string>
<string name="delete_label_confirmation_title">Elimina Etichetta</string>
<string name="delete_label_confirmation_message">Sei sicuro di voler eliminare questa etichetta?</string>
<!-- About -->
<string name="about_version">Versione %1$s</string>
<string name="about_maintainer_title">Responsabile</string>
<string name="about_license_title">Licenza</string>
<string name="about_source_title">Codice sorgente</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Condividi %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">New bill</string>
<string name="action_add_project">Add project</string>
<string name="action_save">Save</string>
<string name="action_edit">Edit</string>
<string name="action_share">Share</string>
<string name="action_search">Search</string>
<string name="action_delete">Delete</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string>
<string name="action_export">Export</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Settle</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Settings</string>
<string name="action_label_bills">Label missing categories</string>
<string name="action_logout">Logout</string>
<string name="action_connect">Connect</string>
<string name="action_discard">Discard</string>
<string name="action_members">Members</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Currencies</string>
<!-- Titles -->
<string name="title_stats">Statistics</string>
<string name="title_edit_project">Edit project</string>
<string name="title_label_bills">Label Bills</string>
<string name="title_labels">Manage Labels</string>
<string name="title_about">About</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Are you sure?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">All bills</string>
<string name="label_categories">Categories</string>
<string name="label_payment_modes">Payment Modes</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Weight</string>
<string name="label_activated">Activated</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server address</string>
<string name="label_username">Username</string>
<string name="label_comment">Comment</string>
<string name="label_what">What?</string>
<string name="label_payer">Who paid?</string>
<string name="label_owers">For whom?</string>
<string name="label_repeat">Repeat every</string>
<string name="label_mode">Mode</string>
<string name="label_category">Category</string>
<string name="label_project_id">Project ID/name</string>
<string name="label_project_title">Project title</string>
<string name="label_use_sso">Use Nextcloud App Account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
<string name="dialog_sync_error_title">Sync error</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">All bills labeled</string>
<string name="msg_no_suggestions">No suggestions</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
<string name="msg_share_web">Link for web browser access.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Stats for %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Loading</string>
<string name="error_no_projects">No projects found</string>
<string name="error_no_members">No members found</string>
<string name="error_no_bills">No bills found</string>
<string name="error_no_member">At least one member required</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_400">400 Bad request</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 Forbidden</string>
<string name="error_404">404 Not Found</string>
<string name="error_sync">Sync failed: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string>
<string name="error_auth">Wrong username or password</string>
<string name="error_json">Invalid server response</string>
<string name="error_req_failed">Request failed</string>
<string name="error_invalid_email">Invalid e-mail</string>
<string name="error_invalid_project_id">Invalid project ID</string>
<string name="error_invalid_project_name">Invalid project title</string>
<string name="error_invalid_bill_name">Invalid bill name</string>
<string name="error_invalid_bill_date">Invalid bill date</string>
<string name="error_invalid_bill_payer">Payer required</string>
<string name="error_invalid_bill_owers">Owers required</string>
<string name="error_no_network">No network connection</string>
<string name="error_server">Server error</string>
<string name="error_io">Server connection broken</string>
<string name="error_share_impossible">Cannot share this project</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Yes</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<!-- Settings -->
<string name="settings_appearance">Appearance</string>
<string name="settings_network">Network</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: \"http\" is unsafe. Use \"https\".</string>
<string name="settings_colorpicker_title">Choose Color</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repeat</string>
<string name="repeat_day">Daily</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Monthly</string>
<string name="repeat_year">Yearly</string>
<string name="payment_mode_none">None</string>
<string name="payment_mode_all">All</string>
<string name="payment_mode_credit_card">Credit card</string>
<string name="payment_mode_cash">Cash</string>
<string name="payment_mode_check">Check</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">None</string>
<string name="category_all">All</string>
<string name="category_all_except_reimbursement">All except reimbursement</string>
<string name="category_groceries">Grocery</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Rent</string>
<string name="category_bills">Bill</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Health</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Reimbursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodation</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">Where</string>
<string name="where_local">Local only</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Join existing project</string>
<string name="todo_create">Create new project</string>
<string name="import_tooltip">Import from file</string>
<string name="choose_account_project_dialog_title">Choose project</string>
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project added successfully.</string>
<string name="no_projects_text">You have no projects yet.</string>
<string name="configure_account_choice">Configure Nextcloud account</string>
<string name="add_project_choice">Add project manually</string>
<string name="no_members_text">No members in this project.</string>
<string name="no_bills_text">No bills found.</string>
<string name="member_already_exists">Member already exists.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File saved: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Invalid date format at row %d</string>
<string name="import_error_owers">Invalid owers at row %d</string>
<string name="add_member_dialog_title">Add Member</string>
<string name="edit_member_dialog_title">Edit Member</string>
<string name="member_edit_delete">Delete</string>
<string name="project_edition_no_change">No changes to save.</string>
<!-- Settlement -->
<string name="center_none">None (Optimal)</string>
<string name="settle_who">Who pays</string>
<string name="settle_to_whom">To whom</string>
<string name="settle_how_much">Amount</string>
<string name="simple_settle_share">Share</string>
<string name="simple_create_bills">Create bills</string>
<string name="settle_bill_what">Settlement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string>
<string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
<string name="main_currency">Main Currency</string>
<!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string>
<string name="label_bills_skip">Skip</string>
<string name="stats_date_min">From</string>
<string name="stats_date_max">To</string>
<string name="stats_who">Member</string>
<string name="stats_paid">Paid</string>
<string name="stats_spent">Spent</string>
<string name="stats_balance">Balance</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
<string name="delete_label_confirmation_title">Delete Label</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Maintainer</string>
<string name="about_license_title">License</string>
<string name="about_source_title">Source code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">新しい請求書</string>
<string name="action_add_project">プロジェクトを追加</string>
<string name="action_save">保存</string>
<string name="action_edit">編集</string>
<string name="action_share">共有</string>
<string name="action_search">検索</string>
<string name="action_delete">削除</string>
<string name="action_archive">アーカイブ</string>
<string name="action_unarchive">アーカイブ解除</string>
<string name="action_export">エクスポート</string>
<string name="action_stats">統計</string>
<string name="action_settle">決済する</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">設定</string>
<string name="action_label_bills">ラベル不足しているカテゴリ</string>
<string name="action_logout">ログアウト</string>
<string name="action_connect">接続する</string>
<string name="action_discard">破棄する</string>
<string name="action_members">メンバー</string>
<string name="action_labels">ラベル</string>
<string name="action_currencies">通貨</string>
<!-- Titles -->
<string name="title_stats">統計情報</string>
<string name="title_edit_project">プロジェクトを編集</string>
<string name="title_label_bills">請求書のラベル</string>
<string name="title_labels">ラベルの管理</string>
<string name="title_about">About</string>
<string name="title_settle">プロジェクトを決済する</string>
<string name="title_share">プロジェクトを共有</string>
<string name="title_add_project">プロジェクトを追加</string>
<string name="title_add_category">カテゴリを追加</string>
<string name="title_add_payment_mode">支払いモードを追加</string>
<string name="title_account">Nextcloudアカウント</string>
<string name="title_share_web">ウェブリンク</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">よろしいですか?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">すべての請求書</string>
<string name="label_categories">カテゴリ</string>
<string name="label_payment_modes">支払いモード</string>
<string name="label_name">名前</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color"></string>
<string name="label_weight">重量</string>
<string name="label_activated">アクティベート済み</string>
<string name="label_password">パスワード</string>
<string name="label_email">電子メール</string>
<string name="label_url">サーバーアドレス</string>
<string name="label_username">ユーザー名</string>
<string name="label_comment">コメント</string>
<string name="label_what">何?</string>
<string name="label_payer">誰が支払ったのですか?</string>
<string name="label_owers">誰のために?</string>
<string name="label_repeat">繰り返します</string>
<string name="label_mode">モード</string>
<string name="label_category">カテゴリ</string>
<string name="label_project_id">プロジェクト ID/名前</string>
<string name="label_project_title">プロジェクトのタイトル</string>
<string name="label_use_sso">Nextcloudアプリアカウントを使用する</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">保存されていない変更</string>
<string name="dialog_unsaved_changes_msg">脱退する前に変更を保存しますか?</string>
<string name="dialog_confirm_remove_project_msg">リモートプロジェクトは削除されません。</string>
<string name="dialog_sync_error_title">同期エラー</string>
<string name="dialog_sync_error_msg">%1$sの同期に失敗しました。\n\n%2$s</string>
<string name="dialog_balanced_msg">費用はすでに均衡しています。</string>
<string name="msg_project_added">プロジェクト %1$s が追加されました</string>
<string name="msg_bill_labeled_done">すべての請求書がラベル付けされました</string>
<string name="msg_no_suggestions">候補なし</string>
<string name="msg_auth_warning">Cospend v0.3.4以上が必要です。</string>
<string name="msg_link_copied">リンクをクリップボードにコピーしました</string>
<string name="msg_share_qr">QRコードをスキャンするか、リンクを共有して参加してください。</string>
<string name="msg_share_web">ウェブブラウザへのアクセスのためのリンク。</string>
<string name="msg_share_qr_warn">Cowspent ユーザーとこのリンクを共有します。</string>
<string name="msg_settle_intro">%1$sの決済:</string>
<string name="msg_settle_sentence">%1$s は %3$.2f を %2$sに借りています</string>
<string name="msg_stats_intro">%1$s の統計:</string>
<string name="msg_stats_header">メンバー (ペイド | 使用済み)</string>
<string name="msg_logged_in_as">%1$s としてログイン中</string>
<!-- Errors -->
<string name="error_generic">エラー</string>
<string name="error_loading">読み込み中</string>
<string name="error_no_projects">プロジェクトが見つかりません</string>
<string name="error_no_members">メンバーが見つかりませんでした</string>
<string name="error_no_bills">請求書が見つかりません</string>
<string name="error_no_member">少なくとも1人のメンバーが必要です</string>
<string name="error_maintenance_mode">サーバーはメンテナンスモードです</string>
<string name="error_400">400件の不正なリクエスト</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 禁止されました</string>
<string name="error_404">404 が見つかりません</string>
<string name="error_sync">同期失敗: %1$s</string>
<string name="error_invalid_login">無効なログイン: %1$s</string>
<string name="error_auth">ユーザー名またはパスワードが間違っています</string>
<string name="error_json">無効なサーバー応答</string>
<string name="error_req_failed">リクエストに失敗しました</string>
<string name="error_invalid_email">無効なメールアドレス</string>
<string name="error_invalid_project_id">無効なプロジェクト ID</string>
<string name="error_invalid_project_name">無効なプロジェクトのタイトル</string>
<string name="error_invalid_bill_name">無効な請求書名</string>
<string name="error_invalid_bill_date">請求書の日付が無効</string>
<string name="error_invalid_bill_payer">支払者が必要です</string>
<string name="error_invalid_bill_owers">所有者が必要</string>
<string name="error_no_network">ネットワーク接続がありません</string>
<string name="error_server">サーバーエラー</string>
<string name="error_io">サーバー接続が切断されました</string>
<string name="error_share_impossible">このプロジェクトを共有できません</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Nextcloudアカウントに接続</string>
<string name="drawer_last_sync">最終同期: %1$02d:%2$02d</string>
<string name="simple_cancel">キャンセル</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">はい</string>
<string name="simple_no">いいえ</string>
<string name="simple_close">閉じる</string>
<!-- Settings -->
<string name="settings_appearance">外観</string>
<string name="settings_network">ネットワーク</string>
<string name="settings_other">その他</string>
<string name="settings_night_mode">テーマ</string>
<string name="settings_offline_mode">オフラインモード</string>
<string name="settings_offline_mode_summary">手動同期のみ。</string>
<string name="settings_color_custom">カスタムカラー</string>
<string name="settings_color_mode">色の選択</string>
<string name="settings_show_archived">アーカイブされたプロジェクトを表示</string>
<string name="settings_beta_features">ベータ版の機能</string>
<string name="settings_beta_features_summary">実験的な機能を有効にします。自己責任で使用してください。</string>
<string name="settings_url_warn_http">警告: \"http\"は安全ではありません。\"https\"を使用してください。</string>
<string name="settings_colorpicker_title">色を選択</string>
<string name="pref_value_color_system">システム</string>
<string name="pref_value_color_server">Nextcloud:</string>
<string name="pref_value_color_manual">マニュアル</string>
<string name="pref_value_theme_light">ライト</string>
<string name="pref_value_theme_dark">ダーク</string>
<string name="pref_value_theme_system">システムをフォロー</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">繰り返しなし</string>
<string name="repeat_day">毎日</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">四十夜</string>
<string name="repeat_month">月ごと</string>
<string name="repeat_year">年ごと</string>
<string name="payment_mode_none">なし</string>
<string name="payment_mode_all">すべて</string>
<string name="payment_mode_credit_card">クレジットカード</string>
<string name="payment_mode_cash">現金</string>
<string name="payment_mode_check">チェック</string>
<string name="payment_mode_online">オンライン</string>
<string name="payment_mode_transfer">送金</string>
<string name="category_none">なし</string>
<string name="category_all">すべて</string>
<string name="category_all_except_reimbursement">払い戻し以外のすべて</string>
<string name="category_groceries">食料品店</string>
<string name="category_leisure">バー/パーティー</string>
<string name="category_rent">レンタル</string>
<string name="category_bills">請求書:</string>
<string name="category_excursion">エクスカーション/文化</string>
<string name="category_health">健康</string>
<string name="category_shopping">ショッピング</string>
<string name="category_reimbursement">払い戻し額</string>
<string name="category_restaurant">レストラン</string>
<string name="category_accomodation">宿泊施設</string>
<string name="category_transport">交通</string>
<string name="category_sport">スポーツ</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">場所</string>
<string name="where_local">ローカルのみ</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">既存のプロジェクトに参加</string>
<string name="todo_create">新しいプロジェクトを作成</string>
<string name="import_tooltip">ファイルからインポート</string>
<string name="choose_account_project_dialog_title">プロジェクトを選択</string>
<string name="choose_account_project_dialog_impossible">このアカウントにはプロジェクトが見つかりません。</string>
<string name="choose_project_management_action">プロジェクト</string>
<string name="project_added_success">プロジェクトが正常に追加されました。</string>
<string name="no_projects_text">プロジェクトはまだありません。</string>
<string name="configure_account_choice">Nextcloudアカウントの設定</string>
<string name="add_project_choice">プロジェクトを手動で追加</string>
<string name="no_members_text">このプロジェクトにはメンバーがいません。</string>
<string name="no_bills_text">請求書が見つかりません。</string>
<string name="member_already_exists">メンバーは既に存在します。</string>
<string name="activity_dialog_title">プロジェクト: %1$s</string>
<string name="remove_project_confirmation">プロジェクト %1$s が削除されました。</string>
<string name="file_saved_success">ファイルを保存しました: %1$s</string>
<string name="import_error_header">%d 行目のインポートに失敗しました</string>
<string name="import_error_date">%d 行目に不正な日付形式があります</string>
<string name="import_error_owers">行 %d に無効な所有者がいます</string>
<string name="add_member_dialog_title">メンバーを追加</string>
<string name="edit_member_dialog_title">メンバーを編集</string>
<string name="member_edit_delete">削除</string>
<string name="project_edition_no_change">保存する変更はありません。</string>
<!-- Settlement -->
<string name="center_none">なし (最適)</string>
<string name="settle_who">誰が支払うか</string>
<string name="settle_to_whom">誰に</string>
<string name="settle_how_much">金額</string>
<string name="simple_settle_share">共有</string>
<string name="simple_create_bills">請求書を作成</string>
<string name="settle_bill_what">受領日</string>
<!-- Currencies -->
<string name="currency_dialog_title">通貨を選択 (%s)</string>
<string name="setting_none">なし</string>
<string name="setting_all">すべて</string>
<string name="currency_saved_success">通貨設定を保存しました。</string>
<string name="main_currency">主な通貨</string>
<!-- Statistics -->
<string name="label_bills_suggested">おすすめのカテゴリ</string>
<string name="label_bills_skip">スキップ</string>
<string name="stats_date_min">差出人:</string>
<string name="stats_date_max">終了日</string>
<string name="stats_who">メンバー</string>
<string name="stats_paid">支払い済み</string>
<string name="stats_spent">使用済み:</string>
<string name="stats_balance">残高</string>
<string name="total">合計: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">接続に失敗しました: %1$s</string>
<string name="error_create_remote_project_helper">作成失敗: %1$s</string>
<string name="error_edit_remote_project_helper">リモートプロジェクトの更新中にエラーが発生しました: %1$s</string>
<string name="remote_project_operation_no_network">リモート操作でネットワークを利用できません。</string>
<string name="error_scanning_bill_qr_code">QRコードの解析に失敗しました。</string>
<string name="error_token_mismatch">認証トークンが一致しません。もう一度ログインしてください。</string>
<string name="insufficient_access_level">このアクションを実行する権限がありません。</string>
<string name="delete_label_confirmation_title">ラベルを削除</string>
<string name="delete_label_confirmation_message">このラベルを削除してもよろしいですか?</string>
<!-- About -->
<string name="about_version">バージョン %1$s</string>
<string name="about_maintainer_title">メンテナー</string>
<string name="about_license_title">ライセンス</string>
<string name="about_source_title">ソース コード</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">プロジェクト %1$s</string>
<string name="share_chooser_title">%1$sを共有</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">New bill</string>
<string name="action_add_project">Add project</string>
<string name="action_save">Save</string>
<string name="action_edit">Edit</string>
<string name="action_share">Share</string>
<string name="action_search">Search</string>
<string name="action_delete">Delete</string>
<string name="action_archive">Archive</string>
<string name="action_unarchive">Unarchive</string>
<string name="action_export">Export</string>
<string name="action_stats">Stats</string>
<string name="action_settle">Settle</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Settings</string>
<string name="action_label_bills">Label missing categories</string>
<string name="action_logout">Logout</string>
<string name="action_connect">Connect</string>
<string name="action_discard">Discard</string>
<string name="action_members">Members</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Currencies</string>
<!-- Titles -->
<string name="title_stats">Statistics</string>
<string name="title_edit_project">Edit project</string>
<string name="title_label_bills">Label Bills</string>
<string name="title_labels">Manage Labels</string>
<string name="title_about">About</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Are you sure?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">All bills</string>
<string name="label_categories">Categories</string>
<string name="label_payment_modes">Payment Modes</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Color</string>
<string name="label_weight">Weight</string>
<string name="label_activated">Activated</string>
<string name="label_password">Password</string>
<string name="label_email">E-mail</string>
<string name="label_url">Server address</string>
<string name="label_username">Username</string>
<string name="label_comment">Comment</string>
<string name="label_what">What?</string>
<string name="label_payer">Who paid?</string>
<string name="label_owers">For whom?</string>
<string name="label_repeat">Repeat every</string>
<string name="label_mode">Mode</string>
<string name="label_category">Category</string>
<string name="label_project_id">Project ID/name</string>
<string name="label_project_title">Project title</string>
<string name="label_use_sso">Use Nextcloud App Account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
<string name="dialog_sync_error_title">Sync error</string>
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
<string name="msg_project_added">Project %1$s added</string>
<string name="msg_bill_labeled_done">All bills labeled</string>
<string name="msg_no_suggestions">No suggestions</string>
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copied to clipboard</string>
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
<string name="msg_share_web">Link for web browser access.</string>
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
<string name="msg_settle_intro">Settlement for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Stats for %1$s:</string>
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
<string name="msg_logged_in_as">Logged in as %1$s</string>
<!-- Errors -->
<string name="error_generic">Error</string>
<string name="error_loading">Loading</string>
<string name="error_no_projects">No projects found</string>
<string name="error_no_members">No members found</string>
<string name="error_no_bills">No bills found</string>
<string name="error_no_member">At least one member required</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_400">400 Bad request</string>
<string name="error_401">401 Unauthorized</string>
<string name="error_403">403 Forbidden</string>
<string name="error_404">404 Not Found</string>
<string name="error_sync">Sync failed: %1$s</string>
<string name="error_invalid_login">Invalid login: %1$s</string>
<string name="error_auth">Wrong username or password</string>
<string name="error_json">Invalid server response</string>
<string name="error_req_failed">Request failed</string>
<string name="error_invalid_email">Invalid e-mail</string>
<string name="error_invalid_project_id">Invalid project ID</string>
<string name="error_invalid_project_name">Invalid project title</string>
<string name="error_invalid_bill_name">Invalid bill name</string>
<string name="error_invalid_bill_date">Invalid bill date</string>
<string name="error_invalid_bill_payer">Payer required</string>
<string name="error_invalid_bill_owers">Owers required</string>
<string name="error_no_network">No network connection</string>
<string name="error_server">Server error</string>
<string name="error_io">Server connection broken</string>
<string name="error_share_impossible">Cannot share this project</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Connect to Nextcloud account</string>
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
<string name="simple_cancel">Cancel</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Yes</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<!-- Settings -->
<string name="settings_appearance">Appearance</string>
<string name="settings_network">Network</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: \"http\" is unsafe. Use \"https\".</string>
<string name="settings_colorpicker_title">Choose Color</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">No repeat</string>
<string name="repeat_day">Daily</string>
<string name="repeat_week">Weekly</string>
<string name="repeat_fortnight">Fortnightly</string>
<string name="repeat_month">Monthly</string>
<string name="repeat_year">Yearly</string>
<string name="payment_mode_none">None</string>
<string name="payment_mode_all">All</string>
<string name="payment_mode_credit_card">Credit card</string>
<string name="payment_mode_cash">Cash</string>
<string name="payment_mode_check">Check</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">None</string>
<string name="category_all">All</string>
<string name="category_all_except_reimbursement">All except reimbursement</string>
<string name="category_groceries">Grocery</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Rent</string>
<string name="category_bills">Bill</string>
<string name="category_excursion">Excursion/Culture</string>
<string name="category_health">Health</string>
<string name="category_shopping">Shopping</string>
<string name="category_reimbursement">Reimbursement</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodation</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">What</string>
<string name="new_project_where">Where</string>
<string name="where_local">Local only</string>
<string name="where_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Join existing project</string>
<string name="todo_create">Create new project</string>
<string name="import_tooltip">Import from file</string>
<string name="choose_account_project_dialog_title">Choose project</string>
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project added successfully.</string>
<string name="no_projects_text">You have no projects yet.</string>
<string name="configure_account_choice">Configure Nextcloud account</string>
<string name="add_project_choice">Add project manually</string>
<string name="no_members_text">No members in this project.</string>
<string name="no_bills_text">No bills found.</string>
<string name="member_already_exists">Member already exists.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s removed.</string>
<string name="file_saved_success">File saved: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Invalid date format at row %d</string>
<string name="import_error_owers">Invalid owers at row %d</string>
<string name="add_member_dialog_title">Add Member</string>
<string name="edit_member_dialog_title">Edit Member</string>
<string name="member_edit_delete">Delete</string>
<string name="project_edition_no_change">No changes to save.</string>
<!-- Settlement -->
<string name="center_none">None (Optimal)</string>
<string name="settle_who">Who pays</string>
<string name="settle_to_whom">To whom</string>
<string name="settle_how_much">Amount</string>
<string name="simple_settle_share">Share</string>
<string name="simple_create_bills">Create bills</string>
<string name="settle_bill_what">Settlement</string>
<!-- Currencies -->
<string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string>
<string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string>
<string name="main_currency">Main Currency</string>
<!-- Statistics -->
<string name="label_bills_suggested">Suggested Categories</string>
<string name="label_bills_skip">Skip</string>
<string name="stats_date_min">From</string>
<string name="stats_date_max">To</string>
<string name="stats_who">Member</string>
<string name="stats_paid">Paid</string>
<string name="stats_spent">Spent</string>
<string name="stats_balance">Balance</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Connection failed: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
<string name="delete_label_confirmation_title">Delete Label</string>
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
<!-- About -->
<string name="about_version">Version %1$s</string>
<string name="about_maintainer_title">Maintainer</string>
<string name="about_license_title">License</string>
<string name="about_source_title">Source code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Gekocht</string>
<!-- Actions -->
<string name="action_new_bill">Nieuwe rekening</string>
<string name="action_add_project">Project toevoegen</string>
<string name="action_save">Opslaan</string>
<string name="action_edit">Bewerken</string>
<string name="action_share">Delen</string>
<string name="action_search">Zoeken</string>
<string name="action_delete">Verwijderen</string>
<string name="action_archive">Archief</string>
<string name="action_unarchive">Dearchiveren</string>
<string name="action_export">Exporteren</string>
<string name="action_stats">Statistieken</string>
<string name="action_settle">Schikken</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Instellingen</string>
<string name="action_label_bills">Label ontbrekende categorieën</string>
<string name="action_logout">Afmelden</string>
<string name="action_connect">Verbinden</string>
<string name="action_discard">Weggooien</string>
<string name="action_members">leden</string>
<string name="action_labels">Labels</string>
<string name="action_currencies">Valuta\'s</string>
<!-- Titles -->
<string name="title_stats">Statistieken</string>
<string name="title_edit_project">Bewerk project</string>
<string name="title_label_bills">Label rekeningen</string>
<string name="title_labels">Labels beheren</string>
<string name="title_about">Informatie</string>
<string name="title_settle">Project schikken</string>
<string name="title_share">Project delen</string>
<string name="title_add_project">Project toevoegen</string>
<string name="title_add_category">Categorie toevoegen</string>
<string name="title_add_payment_mode">Betalingsmodus toevoegen</string>
<string name="title_account">Nextcloud account</string>
<string name="title_share_web">Weblink</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Weet je het zeker?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Alle rekeningen</string>
<string name="label_categories">Categoriechar@@0n</string>
<string name="label_payment_modes">Betalings modi</string>
<string name="label_name">naam</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Kleur</string>
<string name="label_weight">Gewicht</string>
<string name="label_activated">Geactiveerd</string>
<string name="label_password">Wachtwoord</string>
<string name="label_email">E-mailadres</string>
<string name="label_url">Server adres</string>
<string name="label_username">Gebruikersnaam</string>
<string name="label_comment">Opmerking</string>
<string name="label_what">Wat?</string>
<string name="label_payer">Wie heeft betaald?</string>
<string name="label_owers">Voor wie?</string>
<string name="label_repeat">Herhaal elke</string>
<string name="label_mode">Modus</string>
<string name="label_category">categorie</string>
<string name="label_project_id">Project ID/naam</string>
<string name="label_project_title">Project titel</string>
<string name="label_use_sso">Gebruik Nextcloud account</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Niet-opgeslagen wijzigingen</string>
<string name="dialog_unsaved_changes_msg">Wijzigingen opslaan voor verlaten?</string>
<string name="dialog_confirm_remove_project_msg">Het externe project wordt niet verwijderd.</string>
<string name="dialog_sync_error_title">Synchronisatie fout</string>
<string name="dialog_sync_error_msg">Synchronisatie mislukt voor %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Uitgaven zijn al in evenwicht.</string>
<string name="msg_project_added">Project %1$s toegevoegd</string>
<string name="msg_bill_labeled_done">Alle contracten gelabeld</string>
<string name="msg_no_suggestions">Geen suggesties</string>
<string name="msg_auth_warning">Vereist Cospend-v0.3.4+.</string>
<string name="msg_link_copied">Link gekopieerd naar klembord</string>
<string name="msg_share_qr">Scan QR-code of deel de link om je aan te sluiten.</string>
<string name="msg_share_web">Link voor toegang tot de webbrowser.</string>
<string name="msg_share_qr_warn">Deel deze link met een Cowspent gebruiker.</string>
<string name="msg_settle_intro">Afwikkeling voor %1$s:</string>
<string name="msg_settle_sentence">%1$s is %3$.2f verschuldigd aan %2$s</string>
<string name="msg_stats_intro">Statistieken voor %1$s:</string>
<string name="msg_stats_header">Lid ((Paid →Besteed Balance)</string>
<string name="msg_logged_in_as">Ingelogd als %1$s</string>
<!-- Errors -->
<string name="error_generic">Foutmelding</string>
<string name="error_loading">Laden...</string>
<string name="error_no_projects">Geen projecten gevonden</string>
<string name="error_no_members">Geen deelnemers gevonden</string>
<string name="error_no_bills">Geen contracten gevonden</string>
<string name="error_no_member">Ten minste één lid vereist</string>
<string name="error_maintenance_mode">Server is in onderhoudsmodus</string>
<string name="error_400">400 Slechte aanvraag</string>
<string name="error_401">401 Ongeautoriseerd</string>
<string name="error_403">403 Verboden</string>
<string name="error_404">404 niet gevonden</string>
<string name="error_sync">Synchronisatie mislukt: %1$s</string>
<string name="error_invalid_login">Ongeldige login: %1$s</string>
<string name="error_auth">Verkeerde gebruikersnaam of wachtwoord</string>
<string name="error_json">Ongeldige serverreactie</string>
<string name="error_req_failed">Verzoek mislukt</string>
<string name="error_invalid_email">Ongeldig e-mailadres</string>
<string name="error_invalid_project_id">Ongeldig project ID</string>
<string name="error_invalid_project_name">Ongeldige projecttitel</string>
<string name="error_invalid_bill_name">Ongeldige rekeningnaam</string>
<string name="error_invalid_bill_date">Ongeldige rekeningdatum</string>
<string name="error_invalid_bill_payer">Betaler vereist</string>
<string name="error_invalid_bill_owers">Owers vereist</string>
<string name="error_no_network">Geen netwerkverbinding</string>
<string name="error_server">Server fout</string>
<string name="error_io">Verbinding met server verbroken</string>
<string name="error_share_impossible">Kan dit project niet delen</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Verbinding maken met Nextcloud account</string>
<string name="drawer_last_sync">Laatste synchronisatie: %1$02d:%2$02d</string>
<string name="simple_cancel">annuleren</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">ja</string>
<string name="simple_no">Neen</string>
<string name="simple_close">Afsluiten</string>
<!-- Settings -->
<string name="settings_appearance">Uiterlijk</string>
<string name="settings_network">Netwerk</string>
<string name="settings_other">anders</string>
<string name="settings_night_mode">Thema</string>
<string name="settings_offline_mode">Offline modus</string>
<string name="settings_offline_mode_summary">Alleen handmatig synchroniseren.</string>
<string name="settings_color_custom">Aangepaste kleur</string>
<string name="settings_color_mode">Kleur Selectie</string>
<string name="settings_show_archived">Gearchiveerde projecten weergeven</string>
<string name="settings_beta_features">Beta Functies</string>
<string name="settings_beta_features_summary">Experimentele functies inschakelen. Gebruik op eigen risico.</string>
<string name="settings_url_warn_http">WAARSCHUWING: \"http\" is onveilig. Gebruik \"https\".</string>
<string name="settings_colorpicker_title">Kies kleur</string>
<string name="pref_value_color_system">Systeem</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Handleiding</string>
<string name="pref_value_theme_light">Licht</string>
<string name="pref_value_theme_dark">Donker</string>
<string name="pref_value_theme_system">Volg systeem</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Geen herhaling</string>
<string name="repeat_day">Dagelijks</string>
<string name="repeat_week">wekelijks</string>
<string name="repeat_fortnight">Tweewekelijks</string>
<string name="repeat_month">maandelijks</string>
<string name="repeat_year">Jaarlijks</string>
<string name="payment_mode_none">geen</string>
<string name="payment_mode_all">Allemaal</string>
<string name="payment_mode_credit_card">Creditcard bestellen</string>
<string name="payment_mode_cash">Contant</string>
<string name="payment_mode_check">Controleer</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Overschrijving</string>
<string name="category_none">geen</string>
<string name="category_all">Allemaal</string>
<string name="category_all_except_reimbursement">Alles behalve terugbetaling</string>
<string name="category_groceries">Kruidenier</string>
<string name="category_leisure">Bar/Feest</string>
<string name="category_rent">Huur</string>
<string name="category_bills">Rekening</string>
<string name="category_excursion">Excursie/cultuur</string>
<string name="category_health">Gezondheid</string>
<string name="category_shopping">Winkelen</string>
<string name="category_reimbursement">Vergoeding</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Accommodatie</string>
<string name="category_transport">Vervoer</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Wat</string>
<string name="new_project_where">Waar</string>
<string name="where_local">Alleen lokaal</string>
<string name="where_cospend">Coderen</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Deelnemen aan bestaand project</string>
<string name="todo_create">Nieuw project aanmaken</string>
<string name="import_tooltip">Importeren uit bestand</string>
<string name="choose_account_project_dialog_title">Kies project</string>
<string name="choose_account_project_dialog_impossible">Geen projecten gevonden op dit account.</string>
<string name="choose_project_management_action">Project</string>
<string name="project_added_success">Project succesvol toegevoegd.</string>
<string name="no_projects_text">U heeft nog geen projecten.</string>
<string name="configure_account_choice">Configureer Nextcloud account</string>
<string name="add_project_choice">Project handmatig toevoegen</string>
<string name="no_members_text">Geen deelnemers in dit project.</string>
<string name="no_bills_text">Geen contracten gevonden.</string>
<string name="member_already_exists">Lid bestaat al.</string>
<string name="activity_dialog_title">Project: %1$s</string>
<string name="remove_project_confirmation">Project %1$s verwijderd.</string>
<string name="file_saved_success">Bestand opgeslagen: %1$s</string>
<string name="import_error_header">Importeren mislukt op rij %d</string>
<string name="import_error_date">Ongeldige datumnotatie op rij %d</string>
<string name="import_error_owers">Ongeldige owers op rij %d</string>
<string name="add_member_dialog_title">Lid toevoegen</string>
<string name="edit_member_dialog_title">Lid bewerken</string>
<string name="member_edit_delete">Verwijderen</string>
<string name="project_edition_no_change">Geen wijzigingen om op te slaan.</string>
<!-- Settlement -->
<string name="center_none">Geen (Optimaal)</string>
<string name="settle_who">Wie betaalt</string>
<string name="settle_to_whom">Naar wie</string>
<string name="settle_how_much">Hoeveelheid</string>
<string name="simple_settle_share">Delen</string>
<string name="simple_create_bills">Rekeningen aanmaken</string>
<string name="settle_bill_what">Afwikkeling</string>
<!-- Currencies -->
<string name="currency_dialog_title">Kies valuta (%s)</string>
<string name="setting_none">geen</string>
<string name="setting_all">Allemaal</string>
<string name="currency_saved_success">Valuta-instellingen opgeslagen.</string>
<string name="main_currency">Belangrijkste valuta</string>
<!-- Statistics -->
<string name="label_bills_suggested">Voorgestelde categorieën</string>
<string name="label_bills_skip">Overslaan</string>
<string name="stats_date_min">van</string>
<string name="stats_date_max">tot</string>
<string name="stats_who">Lid</string>
<string name="stats_paid">Betaald</string>
<string name="stats_spent">Uitgegeven</string>
<string name="stats_balance">Saldo</string>
<string name="total">Totaal: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Verbinding mislukt: %1$s</string>
<string name="error_create_remote_project_helper">Aanmaken mislukt: %1$s</string>
<string name="error_edit_remote_project_helper">Fout bij het bijwerken van extern project: %1$s</string>
<string name="remote_project_operation_no_network">Netwerk niet beschikbaar voor operatie op afstand.</string>
<string name="error_scanning_bill_qr_code">Fout bij het verwerken van QR-code.</string>
<string name="error_token_mismatch">Authenticatie token komt niet overeen. Log opnieuw in.</string>
<string name="insufficient_access_level">Je hebt geen toestemming om deze actie uit te voeren.</string>
<string name="delete_label_confirmation_title">Label verwijderen</string>
<string name="delete_label_confirmation_message">Weet u zeker dat u dit label wilt verwijderen?</string>
<!-- About -->
<string name="about_version">Versie %1$s</string>
<string name="about_maintainer_title">Beheerder</string>
<string name="about_license_title">Licentie</string>
<string name="about_source_title">Bron code</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Delen %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Kowspent</string>
<!-- Actions -->
<string name="action_new_bill">Ny regning</string>
<string name="action_add_project">Legg til prosjekt</string>
<string name="action_save">Lagre</string>
<string name="action_edit">Rediger</string>
<string name="action_share">Del</string>
<string name="action_search">Søk</string>
<string name="action_delete">Slett</string>
<string name="action_archive">Arkiv</string>
<string name="action_unarchive">Avarkiver</string>
<string name="action_export">Eksporter</string>
<string name="action_stats">Statistikk</string>
<string name="action_settle">Betal</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Innstillinger</string>
<string name="action_label_bills">Etiketten mangler kategorier</string>
<string name="action_logout">Logg</string>
<string name="action_connect">Nettverk</string>
<string name="action_discard">Forkast</string>
<string name="action_members">Medlemmer</string>
<string name="action_labels">Etiketter</string>
<string name="action_currencies">Valutaer</string>
<!-- Titles -->
<string name="title_stats">Statistikk</string>
<string name="title_edit_project">Endre prosjekt</string>
<string name="title_label_bills">Etikett regninger</string>
<string name="title_labels">Administrer etiketter</string>
<string name="title_about">Om</string>
<string name="title_settle">Oppgjør Prosjekt</string>
<string name="title_share">Del prosjeket</string>
<string name="title_add_project">Legg til prosjekt</string>
<string name="title_add_category">Legg til kategori</string>
<string name="title_add_payment_mode">Legg til betalingsmodus</string>
<string name="title_account">Nextcloud konto</string>
<string name="title_share_web">Nettlenke</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Er du sikker?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Alle regninger</string>
<string name="label_categories">Kategorier</string>
<string name="label_payment_modes">Betalings modus</string>
<string name="label_name">Navn</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Farge</string>
<string name="label_weight">Vekt</string>
<string name="label_activated">Aktivert</string>
<string name="label_password">Passord</string>
<string name="label_email">E-post</string>
<string name="label_url">Server adresse</string>
<string name="label_username">Brukernavn</string>
<string name="label_comment">Kommentar</string>
<string name="label_what">Hva?</string>
<string name="label_payer">Hvem betalt?</string>
<string name="label_owers">For hvem?</string>
<string name="label_repeat">Gjenta hver</string>
<string name="label_mode">Modus</string>
<string name="label_category">Kategori</string>
<string name="label_project_id">Prosjektets ID/navn</string>
<string name="label_project_title">Tittel på prosjektet</string>
<string name="label_use_sso">Bruk Nextcloud App-konto</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Ulagrede endringer</string>
<string name="dialog_unsaved_changes_msg">Lagre endringer før du drar?</string>
<string name="dialog_confirm_remove_project_msg">Det eksterne prosjektet vil ikke bli slettet.</string>
<string name="dialog_sync_error_title">Feil med synkronisering</string>
<string name="dialog_sync_error_msg">Synkronisering feilet for %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Utgifter er allerede balanserte.</string>
<string name="msg_project_added">Prosjekt %1$s lagt til</string>
<string name="msg_bill_labeled_done">Alle regninger merket</string>
<string name="msg_no_suggestions">Ingen forslag</string>
<string name="msg_auth_warning">Krever Cospend v0.3.4+.</string>
<string name="msg_link_copied">Kobling kopiert til utklippstavlen</string>
<string name="msg_share_qr">Skann QR-kode eller del lenken for å bli med.</string>
<string name="msg_share_web">Lenke for tilgang til nettleser.</string>
<string name="msg_share_qr_warn">Del denne lenken med en bruker i nærområdet.</string>
<string name="msg_settle_intro">Avgjøring for %1$s:</string>
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
<string name="msg_stats_intro">Statistikk for %1$s:</string>
<string name="msg_stats_header">Medlem (hjelpemiddel for sprøytestatistikk Balance)</string>
<string name="msg_logged_in_as">Logget inn som %1$s</string>
<!-- Errors -->
<string name="error_generic">Feil</string>
<string name="error_loading">Laster</string>
<string name="error_no_projects">Ingen prosjekter funnet</string>
<string name="error_no_members">Ingen medlemmer funnet</string>
<string name="error_no_bills">Ingen regninger funnet</string>
<string name="error_no_member">Minst ett medlem som kreves</string>
<string name="error_maintenance_mode">Serveren er i vedlikeholdsmodus</string>
<string name="error_400">400 ugyldig forespørsel</string>
<string name="error_401">401 Uautorisert</string>
<string name="error_403">403 forbudt</string>
<string name="error_404">404 ikke funnet</string>
<string name="error_sync">Synkronisering feilet: %1$s</string>
<string name="error_invalid_login">Ugyldig innlogging: %1$s</string>
<string name="error_auth">Feil brukernavn eller passord</string>
<string name="error_json">Ugyldig svar fra tjeneren</string>
<string name="error_req_failed">Forespørselen mislyktes</string>
<string name="error_invalid_email">Ugyldig e-post</string>
<string name="error_invalid_project_id">Ugyldig prosjekt-ID</string>
<string name="error_invalid_project_name">Ugyldig prosjekttittel</string>
<string name="error_invalid_bill_name">Ugyldig faktureringsnavn</string>
<string name="error_invalid_bill_date">Ugyldig faktureringsdato</string>
<string name="error_invalid_bill_payer">Betalingsgiver kreves</string>
<string name="error_invalid_bill_owers">Eier kreves</string>
<string name="error_no_network">Ingen nettverkstilkobling</string>
<string name="error_server">Server feil</string>
<string name="error_io">Server-tilkobling ødelagt</string>
<string name="error_share_impossible">Kan ikke dele dette prosjektet</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Koble til Nextcloud konto</string>
<string name="drawer_last_sync">Siste synkronisering: %1$02d:%2$02d</string>
<string name="simple_cancel">Avbryt</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">Ja</string>
<string name="simple_no">Nei</string>
<string name="simple_close">Lukk</string>
<!-- Settings -->
<string name="settings_appearance">Utseende</string>
<string name="settings_network">Nettverk</string>
<string name="settings_other">Annet</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Frakoblet modus</string>
<string name="settings_offline_mode_summary">Bare synkroniser manuelt.</string>
<string name="settings_color_custom">Tilpasset farge</string>
<string name="settings_color_mode">Farge Valg</string>
<string name="settings_show_archived">Vis arkiverte prosjekter</string>
<string name="settings_beta_features">Beta-funksjoner</string>
<string name="settings_beta_features_summary">Aktiver eksperimentelle funksjoner. Bruk på eget ansvar.</string>
<string name="settings_url_warn_http">ADVARSEL: «http» er usikkert. Bruk «https».</string>
<string name="settings_colorpicker_title">Velg farge</string>
<string name="pref_value_color_system">Systemadministrasjon</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manuell</string>
<string name="pref_value_theme_light">Lys</string>
<string name="pref_value_theme_dark">Mørk</string>
<string name="pref_value_theme_system">Følg system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Ingen gjentakelse</string>
<string name="repeat_day">Daglig</string>
<string name="repeat_week">Ukentlig</string>
<string name="repeat_fortnight">Nattlig</string>
<string name="repeat_month">Månedlig</string>
<string name="repeat_year">Årlig</string>
<string name="payment_mode_none">Ingen</string>
<string name="payment_mode_all">Alle</string>
<string name="payment_mode_credit_card">Kredittkort</string>
<string name="payment_mode_cash">Kontant</string>
<string name="payment_mode_check">Sjekk</string>
<string name="payment_mode_online">Pålogget</string>
<string name="payment_mode_transfer">Overfør</string>
<string name="category_none">Ingen</string>
<string name="category_all">Alle</string>
<string name="category_all_except_reimbursement">Alt unntatt refusjon</string>
<string name="category_groceries">Dagligvare</string>
<string name="category_leisure">Bar/Party</string>
<string name="category_rent">Lei</string>
<string name="category_bills">Regning</string>
<string name="category_excursion">Eksurering/Kultur</string>
<string name="category_health">Helse</string>
<string name="category_shopping">Handling</string>
<string name="category_reimbursement">Refusjon</string>
<string name="category_restaurant">Resturant</string>
<string name="category_accomodation">Overnatting</string>
<string name="category_transport">Transporter</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Hva</string>
<string name="new_project_where">Hvor</string>
<string name="where_local">Kun lokalt</string>
<string name="where_cospend">evangeliet</string>
<string name="where_ihatemoney">IHatePenger</string>
<string name="todo_join">Delta i eksisterende prosjekt</string>
<string name="todo_create">Opprett nytt prosjekt</string>
<string name="import_tooltip">Importer fra fil</string>
<string name="choose_account_project_dialog_title">Velg prosjekt</string>
<string name="choose_account_project_dialog_impossible">Ingen prosjekter ble funnet på denne kontoen.</string>
<string name="choose_project_management_action">Prosjekt</string>
<string name="project_added_success">Prosjektet ble lagt til.</string>
<string name="no_projects_text">Du har ingen prosjekter ennå.</string>
<string name="configure_account_choice">Konfigurer Nextcloud konto</string>
<string name="add_project_choice">Legg til prosjekt manuelt</string>
<string name="no_members_text">Ingen medlemmer i dette prosjektet.</string>
<string name="no_bills_text">Ingen regninger funnet.</string>
<string name="member_already_exists">Medlem finnes allerede.</string>
<string name="activity_dialog_title">Prosjekt: %1$s</string>
<string name="remove_project_confirmation">Prosjektet %1$s fjernet.</string>
<string name="file_saved_success">Fil lagret: %1$s</string>
<string name="import_error_header">Import failed at row %d</string>
<string name="import_error_date">Ugyldig dato format ved rad %d</string>
<string name="import_error_owers">Ugyldige doser på rad %d</string>
<string name="add_member_dialog_title">Legg til medlem</string>
<string name="edit_member_dialog_title">Rediger medlem</string>
<string name="member_edit_delete">Slett</string>
<string name="project_edition_no_change">Ingen endringer å lagre.</string>
<!-- Settlement -->
<string name="center_none">Ingen (optimalt)</string>
<string name="settle_who">Hvem betaler</string>
<string name="settle_to_whom">Til hvem</string>
<string name="settle_how_much">Beløp</string>
<string name="simple_settle_share">Del</string>
<string name="simple_create_bills">Opprett regninger</string>
<string name="settle_bill_what">Oppgjør</string>
<!-- Currencies -->
<string name="currency_dialog_title">Velg valuta (%s)</string>
<string name="setting_none">Ingen</string>
<string name="setting_all">Alle</string>
<string name="currency_saved_success">Valuta innstillinger lagret.</string>
<string name="main_currency">Hoved valuta</string>
<!-- Statistics -->
<string name="label_bills_suggested">Foreslåtte kategorier</string>
<string name="label_bills_skip">Hopp</string>
<string name="stats_date_min">Fra</string>
<string name="stats_date_max">Til</string>
<string name="stats_who">Medlem</string>
<string name="stats_paid">Betalt</string>
<string name="stats_spent">Brukt</string>
<string name="stats_balance">Balanse</string>
<string name="total">Totalt: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Tilkobling mislyktes: %1$s</string>
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
<string name="error_edit_remote_project_helper">Feil ved oppdatering av fjernprosjektet: %1$s</string>
<string name="remote_project_operation_no_network">Nettverk er ikke tilgjengelig for ekstern operasjon.</string>
<string name="error_scanning_bill_qr_code">Kunne ikke analysere QR-kode.</string>
<string name="error_token_mismatch">Feil ved godkjenningstoken. Vennligst logg inn på nytt.</string>
<string name="insufficient_access_level">Du har ikke tillatelse til å gjøre denne handlingen.</string>
<string name="delete_label_confirmation_title">Slett etikett</string>
<string name="delete_label_confirmation_message">Er du sikker på at du vil slette dette merket?</string>
<!-- About -->
<string name="about_version">Versjon %1$s</string>
<string name="about_maintainer_title">Vedlikeholdere</string>
<string name="about_license_title">Lisens</string>
<string name="about_source_title">Kildekode</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Prosjekt %1$s</string>
<string name="share_chooser_title">Del %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Krowy wydane</string>
<!-- Actions -->
<string name="action_new_bill">Nowy rachunek</string>
<string name="action_add_project">Dodaj projekt</string>
<string name="action_save">Zapisz</string>
<string name="action_edit">Edytuj</string>
<string name="action_share">Udostępnij</string>
<string name="action_search">Szukaj</string>
<string name="action_delete">Usuń</string>
<string name="action_archive">Archiwum</string>
<string name="action_unarchive">Usuń z archiwum</string>
<string name="action_export">Eksportuj</string>
<string name="action_stats">Statystyki</string>
<string name="action_settle">Utrata</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Ustawienia</string>
<string name="action_label_bills">Brakujące kategorie etykiet</string>
<string name="action_logout">Wyloguj się</string>
<string name="action_connect">Połącz</string>
<string name="action_discard">Odrzuć</string>
<string name="action_members">Członkowie</string>
<string name="action_labels">Etykiety</string>
<string name="action_currencies">Waluty</string>
<!-- Titles -->
<string name="title_stats">Statystyki</string>
<string name="title_edit_project">Edytuj projekt</string>
<string name="title_label_bills">Etykiety rachunków</string>
<string name="title_labels">Zarządzaj etykietami</string>
<string name="title_about">O programie</string>
<string name="title_settle">Ustawiony projekt</string>
<string name="title_share">Udostępnij projekt</string>
<string name="title_add_project">Dodaj projekt</string>
<string name="title_add_category">Dodaj kategorię</string>
<string name="title_add_payment_mode">Dodaj tryb płatności</string>
<string name="title_account">Konto Nextcloud</string>
<string name="title_share_web">Link internetowy</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Jesteś pewien?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Wszystkie rachunki</string>
<string name="label_categories">Kategorie</string>
<string name="label_payment_modes">Tryby płatności</string>
<string name="label_name">Nazwisko</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Kolor</string>
<string name="label_weight">Waga</string>
<string name="label_activated">Aktywowano</string>
<string name="label_password">Hasło</string>
<string name="label_email">E-mail</string>
<string name="label_url">Adres serwera</string>
<string name="label_username">Nazwa użytkownika</string>
<string name="label_comment">Komentarz</string>
<string name="label_what">Co?</string>
<string name="label_payer">Kto zapłacił?</string>
<string name="label_owers">Dla kogo?</string>
<string name="label_repeat">Powtarzaj co</string>
<string name="label_mode">Tryb</string>
<string name="label_category">Kategoria</string>
<string name="label_project_id">Identyfikator/nazwa projektu</string>
<string name="label_project_title">Tytuł projektu</string>
<string name="label_use_sso">Użyj konta aplikacji Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Niezapisane zmiany</string>
<string name="dialog_unsaved_changes_msg">Zapisać zmiany przed wyjściem?</string>
<string name="dialog_confirm_remove_project_msg">Zdalny projekt nie zostanie usunięty.</string>
<string name="dialog_sync_error_title">Błąd synchronizacji</string>
<string name="dialog_sync_error_msg">Synchronizacja %1$snie powiodła się.\n\n%2$s</string>
<string name="dialog_balanced_msg">Wydatki są już zrównoważone.</string>
<string name="msg_project_added">Projekt %1$s dodany</string>
<string name="msg_bill_labeled_done">Wszystkie rachunki oznaczone</string>
<string name="msg_no_suggestions">Brak sugestii</string>
<string name="msg_auth_warning">Wymaga Dołączyć v0.3.4+.</string>
<string name="msg_link_copied">Link skopiowany do schowka</string>
<string name="msg_share_qr">Zeskanuj kod QR lub udostępnij link, aby dołączyć.</string>
<string name="msg_share_web">Link dostępu do przeglądarki internetowej.</string>
<string name="msg_share_qr_warn">Udostępnij ten link użytkownikowi Cowspent</string>
<string name="msg_settle_intro">Rozliczenie dla %1$s:</string>
<string name="msg_settle_sentence">%1$s jest winny %3$.2f do %2$s</string>
<string name="msg_stats_intro">Statystyki dla %1$s:</string>
<string name="msg_stats_header">Członek (Pomoc | Wydano | Saldo)</string>
<string name="msg_logged_in_as">Zalogowano jako %1$s</string>
<!-- Errors -->
<string name="error_generic">Błąd</string>
<string name="error_loading">Ładowanie</string>
<string name="error_no_projects">Nie znaleziono projektów</string>
<string name="error_no_members">Nie znaleziono członków</string>
<string name="error_no_bills">Nie znaleziono rachunków</string>
<string name="error_no_member">Wymagany co najmniej jeden członek</string>
<string name="error_maintenance_mode">Serwer jest w trybie konserwacji</string>
<string name="error_400">400 błędnych żądań</string>
<string name="error_401">401 Nieautoryzowany</string>
<string name="error_403">403 Zabronione</string>
<string name="error_404">404 Nie znaleziono</string>
<string name="error_sync">Synchronizacja nie powiodła się: %1$s</string>
<string name="error_invalid_login">Nieprawidłowy login: %1$s</string>
<string name="error_auth">Nieprawidłowa nazwa użytkownika lub hasło</string>
<string name="error_json">Nieprawidłowa odpowiedź serwera</string>
<string name="error_req_failed">Żądanie nie powiodło się</string>
<string name="error_invalid_email">Nieprawidłowy adres e-mail</string>
<string name="error_invalid_project_id">Nieprawidłowy identyfikator projektu</string>
<string name="error_invalid_project_name">Nieprawidłowy tytuł projektu</string>
<string name="error_invalid_bill_name">Nieprawidłowa nazwa rachunku</string>
<string name="error_invalid_bill_date">Nieprawidłowa data rozliczenia</string>
<string name="error_invalid_bill_payer">Wymagany płatnik</string>
<string name="error_invalid_bill_owers">Wymagane jest posiadanie</string>
<string name="error_no_network">Brak połączenia sieciowego</string>
<string name="error_server">Błąd serwera</string>
<string name="error_io">Połączenie z serwerem przerwane</string>
<string name="error_share_impossible">Nie można udostępnić tego projektu</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Połącz z kontem Nextcloud</string>
<string name="drawer_last_sync">Ostatnia synchronizacja: %1$02d:%2$02d</string>
<string name="simple_cancel">Anuluj</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Tak</string>
<string name="simple_no">Nie</string>
<string name="simple_close">Zamknij</string>
<!-- Settings -->
<string name="settings_appearance">Wygląd</string>
<string name="settings_network">Sieć</string>
<string name="settings_other">Inne</string>
<string name="settings_night_mode">Motyw</string>
<string name="settings_offline_mode">Tryb offline</string>
<string name="settings_offline_mode_summary">Synchronizuj tylko ręcznie.</string>
<string name="settings_color_custom">Własny kolor</string>
<string name="settings_color_mode">Wybór koloru</string>
<string name="settings_show_archived">Pokaż zarchiwizowane projekty</string>
<string name="settings_beta_features">Funkcje Beta</string>
<string name="settings_beta_features_summary">Włącz funkcje eksperymentalne. Używaj na własne ryzyko.</string>
<string name="settings_url_warn_http">OSTRZEŻENIE: \"http\" jest niebezpieczny. Użyj \"https\".</string>
<string name="settings_colorpicker_title">Wybierz kolor</string>
<string name="pref_value_color_system">System</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Ręcznie</string>
<string name="pref_value_theme_light">Światło</string>
<string name="pref_value_theme_dark">Ciemny</string>
<string name="pref_value_theme_system">Śledź system</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Brak powtórzeń</string>
<string name="repeat_day">Codziennie</string>
<string name="repeat_week">Tygodniowo</string>
<string name="repeat_fortnight">Co dwa tygodnie</string>
<string name="repeat_month">Miesięczny</string>
<string name="repeat_year">Rocznie</string>
<string name="payment_mode_none">Brak</string>
<string name="payment_mode_all">Wszystkie</string>
<string name="payment_mode_credit_card">Karta kredytowa</string>
<string name="payment_mode_cash">Gotówka</string>
<string name="payment_mode_check">Sprawdzanie</string>
<string name="payment_mode_online">Dostępny</string>
<string name="payment_mode_transfer">Przelew</string>
<string name="category_none">Brak</string>
<string name="category_all">Wszystkie</string>
<string name="category_all_except_reimbursement">Wszystkie z wyjątkiem zwrotu</string>
<string name="category_groceries">Żywność</string>
<string name="category_leisure">Pasek/Impreza</string>
<string name="category_rent">Wynajem</string>
<string name="category_bills">Rachunek</string>
<string name="category_excursion">Eksploatacja/kultura</string>
<string name="category_health">Zdrowie</string>
<string name="category_shopping">Zakupy</string>
<string name="category_reimbursement">Zwrot kosztów</string>
<string name="category_restaurant">Restauracja</string>
<string name="category_accomodation">Zakwaterowanie</string>
<string name="category_transport">Transport</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Co</string>
<string name="new_project_where">Gdzie</string>
<string name="where_local">Tylko lokalne</string>
<string name="where_cospend">Dołącz</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Dołącz do istniejącego projektu</string>
<string name="todo_create">Utwórz nowy projekt</string>
<string name="import_tooltip">Importuj z pliku</string>
<string name="choose_account_project_dialog_title">Wybierz projekt</string>
<string name="choose_account_project_dialog_impossible">Nie znaleziono projektów na tym koncie.</string>
<string name="choose_project_management_action">Projekt</string>
<string name="project_added_success">Projekt dodany pomyślnie.</string>
<string name="no_projects_text">Nie masz jeszcze żadnych projektów.</string>
<string name="configure_account_choice">Konfiguruj konto Nextcloud</string>
<string name="add_project_choice">Dodaj projekt ręcznie</string>
<string name="no_members_text">Brak członków w tym projekcie.</string>
<string name="no_bills_text">Nie znaleziono rachunków.</string>
<string name="member_already_exists">Użytkownik już istnieje.</string>
<string name="activity_dialog_title">Projekt: %1$s</string>
<string name="remove_project_confirmation">Projekt %1$s usunięty.</string>
<string name="file_saved_success">Plik zapisany: %1$s</string>
<string name="import_error_header">Importowanie nie powiodło się w wierszu %d</string>
<string name="import_error_date">Nieprawidłowy format daty w wierszu %d</string>
<string name="import_error_owers">Niepoprawne owce w wierszu %d</string>
<string name="add_member_dialog_title">Dodaj członka</string>
<string name="edit_member_dialog_title">Edytuj członka</string>
<string name="member_edit_delete">Usuń</string>
<string name="project_edition_no_change">Brak zmian do zapisu.</string>
<!-- Settlement -->
<string name="center_none">Brak (Optymalny)</string>
<string name="settle_who">Kto płaci</string>
<string name="settle_to_whom">Do kogo</string>
<string name="settle_how_much">Kwota</string>
<string name="simple_settle_share">Udostępnij</string>
<string name="simple_create_bills">Utwórz rachunki</string>
<string name="settle_bill_what">Rozliczenie</string>
<!-- Currencies -->
<string name="currency_dialog_title">Wybierz walutę (%s)</string>
<string name="setting_none">Brak</string>
<string name="setting_all">Wszystkie</string>
<string name="currency_saved_success">Zapisano ustawienia waluty.</string>
<string name="main_currency">Waluta główna</string>
<!-- Statistics -->
<string name="label_bills_suggested">Sugerowane kategorie</string>
<string name="label_bills_skip">Pomiń</string>
<string name="stats_date_min">Od</string>
<string name="stats_date_max">Do</string>
<string name="stats_who">Członek</string>
<string name="stats_paid">Zapłacone</string>
<string name="stats_spent">Wydano</string>
<string name="stats_balance">Saldo</string>
<string name="total">Razem: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Połączenie nie powiodło się: %1$s</string>
<string name="error_create_remote_project_helper">Utworzenie nie powiodło się: %1$s</string>
<string name="error_edit_remote_project_helper">Błąd podczas aktualizacji zdalnego projektu: %1$s</string>
<string name="remote_project_operation_no_network">Sieć niedostępna dla zdalnej operacji.</string>
<string name="error_scanning_bill_qr_code">Nie udało się przetworzyć kodu QR.</string>
<string name="error_token_mismatch">Nieprawidłowy token uwierzytelniania. Zaloguj się ponownie.</string>
<string name="insufficient_access_level">Nie masz uprawnień do wykonania tej czynności.</string>
<string name="delete_label_confirmation_title">Usuń etykietę</string>
<string name="delete_label_confirmation_message">Czy na pewno chcesz usunąć tę etykietę?</string>
<!-- About -->
<string name="about_version">Wersja %1$s</string>
<string name="about_maintainer_title">Opiekun</string>
<string name="about_license_title">Licencja</string>
<string name="about_source_title">Kod źródłowy</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projekt %1$s</string>
<string name="share_chooser_title">Udostępnij %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Covarde</string>
<!-- Actions -->
<string name="action_new_bill">Nova fatura</string>
<string name="action_add_project">Adicionar projeto</string>
<string name="action_save">Guardar</string>
<string name="action_edit">Alterar</string>
<string name="action_share">Compartilhar</string>
<string name="action_search">Pesquisa</string>
<string name="action_delete">excluir</string>
<string name="action_archive">Arquivo</string>
<string name="action_unarchive">Desarquivar</string>
<string name="action_export">Exportação</string>
<string name="action_stats">Estatísticas</string>
<string name="action_settle">Liquidar</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Confirgurações</string>
<string name="action_label_bills">Categorias faltando</string>
<string name="action_logout">Desconectar</string>
<string name="action_connect">Conectar</string>
<string name="action_discard">Descartar</string>
<string name="action_members">membros</string>
<string name="action_labels">Marcadores</string>
<string name="action_currencies">Moedas</string>
<!-- Titles -->
<string name="title_stats">estatísticas</string>
<string name="title_edit_project">Editar projeto</string>
<string name="title_label_bills">Rótulo de Faturas</string>
<string name="title_labels">Gerenciar Marcadores</string>
<string name="title_about">SOBRE</string>
<string name="title_settle">Resolver o Projeto</string>
<string name="title_share">Compartilhar Projeto</string>
<string name="title_add_project">Adicionar Projeto</string>
<string name="title_add_category">Adicionar Categoria</string>
<string name="title_add_payment_mode">Adicionar modo de pagamento</string>
<string name="title_account">Conta Nextcloud</string>
<string name="title_share_web">Link da Web</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Você tem certeza?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Todas as contas</string>
<string name="label_categories">categorias</string>
<string name="label_payment_modes">Modos de Pagamento</string>
<string name="label_name">Nome:</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Cor</string>
<string name="label_weight">Peso</string>
<string name="label_activated">Ativado</string>
<string name="label_password">Palavra-passe</string>
<string name="label_email">e-mail</string>
<string name="label_url">Endereço do servidor</string>
<string name="label_username">Usuário:</string>
<string name="label_comment">Comentar</string>
<string name="label_what">Quê?</string>
<string name="label_payer">Quem pagou?</string>
<string name="label_owers">Para quem?</string>
<string name="label_repeat">Repita a cada</string>
<string name="label_mode">Modo</string>
<string name="label_category">categoria</string>
<string name="label_project_id">ID/nome do projeto</string>
<string name="label_project_title">Título do projeto</string>
<string name="label_use_sso">Usar Conta do App Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Alterações não salvas</string>
<string name="dialog_unsaved_changes_msg">Salvar as alterações antes de sair?</string>
<string name="dialog_confirm_remove_project_msg">O projeto remoto não será excluído.</string>
<string name="dialog_sync_error_title">Erro de sincronização</string>
<string name="dialog_sync_error_msg">A sincronização falhou para %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">As despesas já estão equilibradas.</string>
<string name="msg_project_added">Projeto %1$s adicionado</string>
<string name="msg_bill_labeled_done">Todas as faturas rotuladas</string>
<string name="msg_no_suggestions">Não há sugestões</string>
<string name="msg_auth_warning">Requer Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copiado para área de transferência</string>
<string name="msg_share_qr">Escaneie o código QR ou compartilhe o link para entrar.</string>
<string name="msg_share_web">Link para acesso ao navegador web.</string>
<string name="msg_share_qr_warn">Compartilhe este link com um usuário CowGed.</string>
<string name="msg_settle_intro">Assentamento para %1$s:</string>
<string name="msg_settle_sentence">%1$s deve %3$.2f a %2$s</string>
<string name="msg_stats_intro">Estatísticas para %1$s:</string>
<string name="msg_stats_header">Membro (Ajuda-Gasto Balanço)</string>
<string name="msg_logged_in_as">Sessão iniciada como %1$s</string>
<!-- Errors -->
<string name="error_generic">ERRO</string>
<string name="error_loading">Carregando...</string>
<string name="error_no_projects">Nenhum projeto encontrado</string>
<string name="error_no_members">Nenhum membro encontrado</string>
<string name="error_no_bills">Nenhuma conta encontrada</string>
<string name="error_no_member">Pelo menos um membro é obrigatório</string>
<string name="error_maintenance_mode">O servidor está em modo de manutenção</string>
<string name="error_400">Solicitação inválida</string>
<string name="error_401">401 Não Autorizado</string>
<string name="error_403">403 Proibido</string>
<string name="error_404">404 Não Encontrado</string>
<string name="error_sync">Falha de sincronização: %1$s</string>
<string name="error_invalid_login">Login inválido: %1$s</string>
<string name="error_auth">Nome de usuário ou senha incorretos</string>
<string name="error_json">Resposta do servidor inválida</string>
<string name="error_req_failed">Falha na solicitação</string>
<string name="error_invalid_email">E-mail inválido</string>
<string name="error_invalid_project_id">ID do projeto inválido</string>
<string name="error_invalid_project_name">Título do projeto inválido</string>
<string name="error_invalid_bill_name">Nome de conta inválido</string>
<string name="error_invalid_bill_date">Data de fatura inválida</string>
<string name="error_invalid_bill_payer">Pagador necessário</string>
<string name="error_invalid_bill_owers">Proprietários obrigatórios</string>
<string name="error_no_network">Nenhuma conexão de rede</string>
<string name="error_server">Erro no servidor</string>
<string name="error_io">Conexão com o servidor interrompida</string>
<string name="error_share_impossible">Não é possível compartilhar este projeto</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Conectar à conta da Nextcloud</string>
<string name="drawer_last_sync">Última sincronização: %1$02d:%2$02d</string>
<string name="simple_cancel">cancelar</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">sim</string>
<string name="simple_no">Não</string>
<string name="simple_close">FECHAR</string>
<!-- Settings -->
<string name="settings_appearance">Aparência</string>
<string name="settings_network">Rede</string>
<string name="settings_other">Outros</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Modo offline</string>
<string name="settings_offline_mode_summary">Sincronizar apenas manualmente.</string>
<string name="settings_color_custom">Cor personalizada</string>
<string name="settings_color_mode">Seleção de Cor</string>
<string name="settings_show_archived">Mostrar projetos arquivados</string>
<string name="settings_beta_features">Recursos Beta</string>
<string name="settings_beta_features_summary">Habilite recursos experimentais. Use a seu próprio risco.</string>
<string name="settings_url_warn_http">AVISO: \"http\" não é seguro. Use \"https\".</string>
<string name="settings_colorpicker_title">Escolha a cor</string>
<string name="pref_value_color_system">SISTEMA</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manualmente</string>
<string name="pref_value_theme_light">Fino</string>
<string name="pref_value_theme_dark">Escuro</string>
<string name="pref_value_theme_system">Seguir sistema</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Não repetir</string>
<string name="repeat_day">Diariamente</string>
<string name="repeat_week">Semanalmente</string>
<string name="repeat_fortnight">Quinzenal</string>
<string name="repeat_month">Mensual</string>
<string name="repeat_year">Anualmente</string>
<string name="payment_mode_none">Nenhuma</string>
<string name="payment_mode_all">TODOS</string>
<string name="payment_mode_credit_card">Cartão de crédito</string>
<string name="payment_mode_cash">Dinheiro</string>
<string name="payment_mode_check">Verificar</string>
<string name="payment_mode_online">Disponível</string>
<string name="payment_mode_transfer">Transferência</string>
<string name="category_none">Nenhuma</string>
<string name="category_all">TODOS</string>
<string name="category_all_except_reimbursement">Todos, exceto o reembolso</string>
<string name="category_groceries">Mercearia</string>
<string name="category_leisure">Barra/Party</string>
<string name="category_rent">Aluguel</string>
<string name="category_bills">Faturar</string>
<string name="category_excursion">Excursão/Cultura</string>
<string name="category_health">Vida</string>
<string name="category_shopping">Compras</string>
<string name="category_reimbursement">Reembolso</string>
<string name="category_restaurant">Restaurante</string>
<string name="category_accomodation">Acomodação</string>
<string name="category_transport">Transporte</string>
<string name="category_sport">Esporte</string>
<!-- Project specific -->
<string name="new_project_action">O</string>
<string name="new_project_where">Onde</string>
<string name="where_local">Apenas local</string>
<string name="where_cospend">Cosdar</string>
<string name="where_ihatemoney">Dinheiro</string>
<string name="todo_join">Junte-se ao projeto existente</string>
<string name="todo_create">Criar novo projeto</string>
<string name="import_tooltip">Importar do arquivo</string>
<string name="choose_account_project_dialog_title">Escolher projeto</string>
<string name="choose_account_project_dialog_impossible">Nenhum projeto encontrado nesta conta.</string>
<string name="choose_project_management_action">Projecto</string>
<string name="project_added_success">Projeto adicionado com sucesso.</string>
<string name="no_projects_text">Você ainda não tem nenhum projeto.</string>
<string name="configure_account_choice">Configurar conta Nextcloud</string>
<string name="add_project_choice">Adicionar projeto manualmente</string>
<string name="no_members_text">Nenhum membro neste projeto.</string>
<string name="no_bills_text">Nenhuma conta encontrada.</string>
<string name="member_already_exists">Membro já existe.</string>
<string name="activity_dialog_title">Projeto: %1$s</string>
<string name="remove_project_confirmation">Projeto %1$s removido.</string>
<string name="file_saved_success">Arquivo salvo: %1$s</string>
<string name="import_error_header">Importação falhou na linha %d</string>
<string name="import_error_date">Formato de data inválido na linha %d</string>
<string name="import_error_owers">Linhas inválidas na linha %d</string>
<string name="add_member_dialog_title">Adicionar Membro</string>
<string name="edit_member_dialog_title">Editar Membro</string>
<string name="member_edit_delete">excluir</string>
<string name="project_edition_no_change">Sem alterações para salvar.</string>
<!-- Settlement -->
<string name="center_none">Nenhum (Profissional)</string>
<string name="settle_who">Quem paga</string>
<string name="settle_to_whom">A quem</string>
<string name="settle_how_much">Quantidade</string>
<string name="simple_settle_share">Compartilhar</string>
<string name="simple_create_bills">Criar faturas</string>
<string name="settle_bill_what">Liquidação</string>
<!-- Currencies -->
<string name="currency_dialog_title">Escolher Moeda (%s)</string>
<string name="setting_none">Nenhuma</string>
<string name="setting_all">TODOS</string>
<string name="currency_saved_success">Configurações de moeda salvas.</string>
<string name="main_currency">Moeda Principal</string>
<!-- Statistics -->
<string name="label_bills_suggested">Categorias Sugeridas</string>
<string name="label_bills_skip">Ignorar</string>
<string name="stats_date_min">De</string>
<string name="stats_date_max">Para</string>
<string name="stats_who">Membro</string>
<string name="stats_paid">Paga</string>
<string name="stats_spent">Gasto</string>
<string name="stats_balance">Saldo</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">A conexão falhou: %1$s</string>
<string name="error_create_remote_project_helper">Falha na criação: %1$s</string>
<string name="error_edit_remote_project_helper">Erro ao atualizar projeto remoto: %1$s</string>
<string name="remote_project_operation_no_network">Rede indisponível para operação remota.</string>
<string name="error_scanning_bill_qr_code">Falha ao analisar o código QR.</string>
<string name="error_token_mismatch">Token de autenticação incompatível. Por favor, faça login novamente.</string>
<string name="insufficient_access_level">Você não tem permissão para executar esta ação</string>
<string name="delete_label_confirmation_title">Excluir etiqueta</string>
<string name="delete_label_confirmation_message">Você tem certeza que deseja excluir este marcador?</string>
<!-- About -->
<string name="about_version">Versão %1$s</string>
<string name="about_maintainer_title">Mantenedor</string>
<string name="about_license_title">Tipo:</string>
<string name="about_source_title">Código fonte</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projeto %1$s</string>
<string name="share_chooser_title">Compartilhar %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Covarde</string>
<!-- Actions -->
<string name="action_new_bill">Nova fatura</string>
<string name="action_add_project">Adicionar projeto</string>
<string name="action_save">Guardar</string>
<string name="action_edit">Alterar</string>
<string name="action_share">Compartilhar</string>
<string name="action_search">Pesquisa</string>
<string name="action_delete">excluir</string>
<string name="action_archive">Arquivo</string>
<string name="action_unarchive">Desarquivar</string>
<string name="action_export">Exportação</string>
<string name="action_stats">Estatísticas</string>
<string name="action_settle">Liquidar</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Confirgurações</string>
<string name="action_label_bills">Categorias faltando</string>
<string name="action_logout">Desconectar</string>
<string name="action_connect">Conectar</string>
<string name="action_discard">Descartar</string>
<string name="action_members">membros</string>
<string name="action_labels">Marcadores</string>
<string name="action_currencies">Moedas</string>
<!-- Titles -->
<string name="title_stats">estatísticas</string>
<string name="title_edit_project">Editar projeto</string>
<string name="title_label_bills">Rótulo de Faturas</string>
<string name="title_labels">Gerenciar Marcadores</string>
<string name="title_about">SOBRE</string>
<string name="title_settle">Resolver o Projeto</string>
<string name="title_share">Compartilhar Projeto</string>
<string name="title_add_project">Adicionar Projeto</string>
<string name="title_add_category">Adicionar Categoria</string>
<string name="title_add_payment_mode">Adicionar modo de pagamento</string>
<string name="title_account">Conta Nextcloud</string>
<string name="title_share_web">Link da Web</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Você tem certeza?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Todas as contas</string>
<string name="label_categories">categorias</string>
<string name="label_payment_modes">Modos de Pagamento</string>
<string name="label_name">Nome:</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Cor</string>
<string name="label_weight">Peso</string>
<string name="label_activated">Ativado</string>
<string name="label_password">Palavra-passe</string>
<string name="label_email">e-mail</string>
<string name="label_url">Endereço do servidor</string>
<string name="label_username">Usuário:</string>
<string name="label_comment">Comentar</string>
<string name="label_what">Quê?</string>
<string name="label_payer">Quem pagou?</string>
<string name="label_owers">Para quem?</string>
<string name="label_repeat">Repita a cada</string>
<string name="label_mode">Modo</string>
<string name="label_category">categoria</string>
<string name="label_project_id">ID/nome do projeto</string>
<string name="label_project_title">Título do projeto</string>
<string name="label_use_sso">Usar Conta do App Nextcloud</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Alterações não salvas</string>
<string name="dialog_unsaved_changes_msg">Salvar as alterações antes de sair?</string>
<string name="dialog_confirm_remove_project_msg">O projeto remoto não será excluído.</string>
<string name="dialog_sync_error_title">Erro de sincronização</string>
<string name="dialog_sync_error_msg">A sincronização falhou para %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">As despesas já estão equilibradas.</string>
<string name="msg_project_added">Projeto %1$s adicionado</string>
<string name="msg_bill_labeled_done">Todas as faturas rotuladas</string>
<string name="msg_no_suggestions">Não há sugestões</string>
<string name="msg_auth_warning">Requer Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copiado para área de transferência</string>
<string name="msg_share_qr">Escaneie o código QR ou compartilhe o link para entrar.</string>
<string name="msg_share_web">Link para acesso ao navegador web.</string>
<string name="msg_share_qr_warn">Compartilhe este link com um usuário CowGed.</string>
<string name="msg_settle_intro">Assentamento para %1$s:</string>
<string name="msg_settle_sentence">%1$s deve %3$.2f a %2$s</string>
<string name="msg_stats_intro">Estatísticas para %1$s:</string>
<string name="msg_stats_header">Membro (Ajuda-Gasto Balanço)</string>
<string name="msg_logged_in_as">Sessão iniciada como %1$s</string>
<!-- Errors -->
<string name="error_generic">ERRO</string>
<string name="error_loading">Carregando...</string>
<string name="error_no_projects">Nenhum projeto encontrado</string>
<string name="error_no_members">Nenhum membro encontrado</string>
<string name="error_no_bills">Nenhuma conta encontrada</string>
<string name="error_no_member">Pelo menos um membro é obrigatório</string>
<string name="error_maintenance_mode">O servidor está em modo de manutenção</string>
<string name="error_400">Solicitação inválida</string>
<string name="error_401">401 Não Autorizado</string>
<string name="error_403">403 Proibido</string>
<string name="error_404">404 Não Encontrado</string>
<string name="error_sync">Falha de sincronização: %1$s</string>
<string name="error_invalid_login">Login inválido: %1$s</string>
<string name="error_auth">Nome de usuário ou senha incorretos</string>
<string name="error_json">Resposta do servidor inválida</string>
<string name="error_req_failed">Falha na solicitação</string>
<string name="error_invalid_email">E-mail inválido</string>
<string name="error_invalid_project_id">ID do projeto inválido</string>
<string name="error_invalid_project_name">Título do projeto inválido</string>
<string name="error_invalid_bill_name">Nome de conta inválido</string>
<string name="error_invalid_bill_date">Data de fatura inválida</string>
<string name="error_invalid_bill_payer">Pagador necessário</string>
<string name="error_invalid_bill_owers">Proprietários obrigatórios</string>
<string name="error_no_network">Nenhuma conexão de rede</string>
<string name="error_server">Erro no servidor</string>
<string name="error_io">Conexão com o servidor interrompida</string>
<string name="error_share_impossible">Não é possível compartilhar este projeto</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Conectar à conta da Nextcloud</string>
<string name="drawer_last_sync">Última sincronização: %1$02d:%2$02d</string>
<string name="simple_cancel">cancelar</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">OK</string>
<string name="simple_yes">sim</string>
<string name="simple_no">Não</string>
<string name="simple_close">FECHAR</string>
<!-- Settings -->
<string name="settings_appearance">Aparência</string>
<string name="settings_network">Rede</string>
<string name="settings_other">Outros</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Modo offline</string>
<string name="settings_offline_mode_summary">Sincronizar apenas manualmente.</string>
<string name="settings_color_custom">Cor personalizada</string>
<string name="settings_color_mode">Seleção de Cor</string>
<string name="settings_show_archived">Mostrar projetos arquivados</string>
<string name="settings_beta_features">Recursos Beta</string>
<string name="settings_beta_features_summary">Habilite recursos experimentais. Use a seu próprio risco.</string>
<string name="settings_url_warn_http">AVISO: \"http\" não é seguro. Use \"https\".</string>
<string name="settings_colorpicker_title">Escolha a cor</string>
<string name="pref_value_color_system">SISTEMA</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manualmente</string>
<string name="pref_value_theme_light">Fino</string>
<string name="pref_value_theme_dark">Escuro</string>
<string name="pref_value_theme_system">Seguir sistema</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Não repetir</string>
<string name="repeat_day">Diariamente</string>
<string name="repeat_week">Semanalmente</string>
<string name="repeat_fortnight">Quinzenal</string>
<string name="repeat_month">Mensual</string>
<string name="repeat_year">Anualmente</string>
<string name="payment_mode_none">Nenhuma</string>
<string name="payment_mode_all">TODOS</string>
<string name="payment_mode_credit_card">Cartão de crédito</string>
<string name="payment_mode_cash">Dinheiro</string>
<string name="payment_mode_check">Verificar</string>
<string name="payment_mode_online">Disponível</string>
<string name="payment_mode_transfer">Transferência</string>
<string name="category_none">Nenhuma</string>
<string name="category_all">TODOS</string>
<string name="category_all_except_reimbursement">Todos, exceto o reembolso</string>
<string name="category_groceries">Mercearia</string>
<string name="category_leisure">Barra/Party</string>
<string name="category_rent">Aluguel</string>
<string name="category_bills">Faturar</string>
<string name="category_excursion">Excursão/Cultura</string>
<string name="category_health">Vida</string>
<string name="category_shopping">Compras</string>
<string name="category_reimbursement">Reembolso</string>
<string name="category_restaurant">Restaurante</string>
<string name="category_accomodation">Acomodação</string>
<string name="category_transport">Transporte</string>
<string name="category_sport">Esporte</string>
<!-- Project specific -->
<string name="new_project_action">O</string>
<string name="new_project_where">Onde</string>
<string name="where_local">Apenas local</string>
<string name="where_cospend">Cosdar</string>
<string name="where_ihatemoney">Dinheiro</string>
<string name="todo_join">Junte-se ao projeto existente</string>
<string name="todo_create">Criar novo projeto</string>
<string name="import_tooltip">Importar do arquivo</string>
<string name="choose_account_project_dialog_title">Escolher projeto</string>
<string name="choose_account_project_dialog_impossible">Nenhum projeto encontrado nesta conta.</string>
<string name="choose_project_management_action">Projecto</string>
<string name="project_added_success">Projeto adicionado com sucesso.</string>
<string name="no_projects_text">Você ainda não tem nenhum projeto.</string>
<string name="configure_account_choice">Configurar conta Nextcloud</string>
<string name="add_project_choice">Adicionar projeto manualmente</string>
<string name="no_members_text">Nenhum membro neste projeto.</string>
<string name="no_bills_text">Nenhuma conta encontrada.</string>
<string name="member_already_exists">Membro já existe.</string>
<string name="activity_dialog_title">Projeto: %1$s</string>
<string name="remove_project_confirmation">Projeto %1$s removido.</string>
<string name="file_saved_success">Arquivo salvo: %1$s</string>
<string name="import_error_header">Importação falhou na linha %d</string>
<string name="import_error_date">Formato de data inválido na linha %d</string>
<string name="import_error_owers">Linhas inválidas na linha %d</string>
<string name="add_member_dialog_title">Adicionar Membro</string>
<string name="edit_member_dialog_title">Editar Membro</string>
<string name="member_edit_delete">excluir</string>
<string name="project_edition_no_change">Sem alterações para salvar.</string>
<!-- Settlement -->
<string name="center_none">Nenhum (Profissional)</string>
<string name="settle_who">Quem paga</string>
<string name="settle_to_whom">A quem</string>
<string name="settle_how_much">Quantidade</string>
<string name="simple_settle_share">Compartilhar</string>
<string name="simple_create_bills">Criar faturas</string>
<string name="settle_bill_what">Liquidação</string>
<!-- Currencies -->
<string name="currency_dialog_title">Escolher Moeda (%s)</string>
<string name="setting_none">Nenhuma</string>
<string name="setting_all">TODOS</string>
<string name="currency_saved_success">Configurações de moeda salvas.</string>
<string name="main_currency">Moeda Principal</string>
<!-- Statistics -->
<string name="label_bills_suggested">Categorias Sugeridas</string>
<string name="label_bills_skip">Ignorar</string>
<string name="stats_date_min">De</string>
<string name="stats_date_max">Para</string>
<string name="stats_who">Membro</string>
<string name="stats_paid">Paga</string>
<string name="stats_spent">Gasto</string>
<string name="stats_balance">Saldo</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">A conexão falhou: %1$s</string>
<string name="error_create_remote_project_helper">Falha na criação: %1$s</string>
<string name="error_edit_remote_project_helper">Erro ao atualizar projeto remoto: %1$s</string>
<string name="remote_project_operation_no_network">Rede indisponível para operação remota.</string>
<string name="error_scanning_bill_qr_code">Falha ao analisar o código QR.</string>
<string name="error_token_mismatch">Token de autenticação incompatível. Por favor, faça login novamente.</string>
<string name="insufficient_access_level">Você não tem permissão para executar esta ação</string>
<string name="delete_label_confirmation_title">Excluir etiqueta</string>
<string name="delete_label_confirmation_message">Você tem certeza que deseja excluir este marcador?</string>
<!-- About -->
<string name="about_version">Versão %1$s</string>
<string name="about_maintainer_title">Mantenedor</string>
<string name="about_license_title">Tipo:</string>
<string name="about_source_title">Código fonte</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Projeto %1$s</string>
<string name="share_chooser_title">Compartilhar %1$s</string>
</resources>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Coafă</string>
<!-- Actions -->
<string name="action_new_bill">Factură nouă</string>
<string name="action_add_project">Adaugă proiect</string>
<string name="action_save">Salvează</string>
<string name="action_edit">Editare</string>
<string name="action_share">Distribuie</string>
<string name="action_search">Caută</string>
<string name="action_delete">Ștergere</string>
<string name="action_archive">Arhivează</string>
<string name="action_unarchive">Dezarhivare</string>
<string name="action_export">Exportă</string>
<string name="action_stats">Statistici</string>
<string name="action_settle">Setare</string>
<string name="action_scan_qrcode">Scan QR Code</string>
<string name="action_settings">Setări</string>
<string name="action_label_bills">Categorii lipsă etichetă</string>
<string name="action_logout">Deconectare</string>
<string name="action_connect">Conectează-te</string>
<string name="action_discard">Renunțați</string>
<string name="action_members">Membri</string>
<string name="action_labels">Etichete</string>
<string name="action_currencies">Monede</string>
<!-- Titles -->
<string name="title_stats">Statistici</string>
<string name="title_edit_project">Editare proiect</string>
<string name="title_label_bills">Etichetă factură</string>
<string name="title_labels">Gestionare etichete</string>
<string name="title_about">Despre</string>
<string name="title_settle">Settle Project</string>
<string name="title_share">Partajează proiect</string>
<string name="title_add_project">Adaugă Proiect</string>
<string name="title_add_category">Adaugă Categorie</string>
<string name="title_add_payment_mode">Adaugă Mod de Plată</string>
<string name="title_account">Cont Nextcloud</string>
<string name="title_share_web">Link web</string>
<string name="title_share_qr">Cowspent link</string>
<string name="title_confirm">Ești sigur?</string>
<!-- Labels and Fields -->
<string name="label_all_bills">Toate facturile</string>
<string name="label_categories">Categorii</string>
<string name="label_payment_modes">Moduri de plată</string>
<string name="label_name">Nume</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Culoare</string>
<string name="label_weight">Greutate</string>
<string name="label_activated">Activat</string>
<string name="label_password">Parolă</string>
<string name="label_email">E-mail</string>
<string name="label_url">Adresa serverului</string>
<string name="label_username">Nume</string>
<string name="label_comment">Comentariu</string>
<string name="label_what">Ce?</string>
<string name="label_payer">Cine a plătit?</string>
<string name="label_owers">Pentru cine?</string>
<string name="label_repeat">Repetă fiecare</string>
<string name="label_mode">Mod</string>
<string name="label_category">Categorie</string>
<string name="label_project_id">ID/nume proiect</string>
<string name="label_project_title">Titlul proiectului</string>
<string name="label_use_sso">Utilizați contul de Nextcloud App</string>
<!-- Dialogs and Messages -->
<string name="dialog_unsaved_changes_title">Modificări nesalvate</string>
<string name="dialog_unsaved_changes_msg">Salvați modificările înainte de a pleca?</string>
<string name="dialog_confirm_remove_project_msg">Proiectul la distanță nu va fi șters.</string>
<string name="dialog_sync_error_title">Eroare de sincronizare</string>
<string name="dialog_sync_error_msg">Sincronizarea a eșuat pentru %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Cheltuielile sunt deja echilibrate.</string>
<string name="msg_project_added">Proiectul %1$s a fost adăugat</string>
<string name="msg_bill_labeled_done">Toate facturile etichetate</string>
<string name="msg_no_suggestions">Fără sugestii</string>
<string name="msg_auth_warning">Necesită Cospend v0.3.4+.</string>
<string name="msg_link_copied">Link copiat în clipboard</string>
<string name="msg_share_qr">Scanați codul QR sau partajați link-ul pentru a vă alătura.</string>
<string name="msg_share_web">Link pentru accesul browser-ului web.</string>
<string name="msg_share_qr_warn">Partajează acest link cu un utilizator Cowcheltuit.</string>
<string name="msg_settle_intro">Decontare pentru %1$s:</string>
<string name="msg_settle_sentence">%1$s datorează %3$.2f la %2$s</string>
<string name="msg_stats_intro">Statistici pentru %1$s:</string>
<string name="msg_stats_header">Membru (Ajutor = Balans cheltuit)</string>
<string name="msg_logged_in_as">Autentificat ca %1$s</string>
<!-- Errors -->
<string name="error_generic">Eroare</string>
<string name="error_loading">Încărcare</string>
<string name="error_no_projects">Niciun proiect găsit</string>
<string name="error_no_members">Niciun membru găsit</string>
<string name="error_no_bills">Nicio factură găsită</string>
<string name="error_no_member">Cel puţin un membru este necesar</string>
<string name="error_maintenance_mode">Serverul este în modul de mentenanță</string>
<string name="error_400">Cerere invalidă 400</string>
<string name="error_401">401 Neautorizat</string>
<string name="error_403">403 Interzis</string>
<string name="error_404">404 Nu a fost găsit</string>
<string name="error_sync">Sincronizare esuata: %1$s</string>
<string name="error_invalid_login">Autentificare invalidă: %1$s</string>
<string name="error_auth">Nume utilizator sau parolă greșite</string>
<string name="error_json">Răspuns server nevalid</string>
<string name="error_req_failed">Solicitare eșuată</string>
<string name="error_invalid_email">E-mail invalid</string>
<string name="error_invalid_project_id">Identificator proiect invalid</string>
<string name="error_invalid_project_name">Titlu proiect nevalid</string>
<string name="error_invalid_bill_name">Nume factură nevalid</string>
<string name="error_invalid_bill_date">Data facturii invalidă</string>
<string name="error_invalid_bill_payer">Plătitor necesar</string>
<string name="error_invalid_bill_owers">Proprietari necesari</string>
<string name="error_no_network">Nicio conexiune la rețea</string>
<string name="error_server">Eroare server</string>
<string name="error_io">Conexiunea serverului este întreruptă</string>
<string name="error_share_impossible">Nu se poate partaja acest proiect</string>
<!-- Drawer / Common UI -->
<string name="drawer_no_account">Conectare la contul Nextcloud</string>
<string name="drawer_last_sync">Ultima sincronizare: %1$02d:%2$02d</string>
<string name="simple_cancel">Anulează</string>
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
<string name="simple_yes">Da</string>
<string name="simple_no">Nr</string>
<string name="simple_close">Inchide</string>
<!-- Settings -->
<string name="settings_appearance">Aspectul</string>
<string name="settings_network">Rețea</string>
<string name="settings_other">Altele</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Mod offline</string>
<string name="settings_offline_mode_summary">Sincronizează doar manual.</string>
<string name="settings_color_custom">Culoare personalizată</string>
<string name="settings_color_mode">Selectare culoare</string>
<string name="settings_show_archived">Arată proiectele arhivate</string>
<string name="settings_beta_features">Caracteristici Beta</string>
<string name="settings_beta_features_summary">Activați funcțiile experimentale. Utilizați pe propriul risc.</string>
<string name="settings_url_warn_http">AVERTISMENT: \"http\" nu este sigur. Utilizaţi \"http\".</string>
<string name="settings_colorpicker_title">Alege culoare</string>
<string name="pref_value_color_system">Sistem</string>
<string name="pref_value_color_server">Nextcloud</string>
<string name="pref_value_color_manual">Manual</string>
<string name="pref_value_theme_light">Lumină</string>
<string name="pref_value_theme_dark">Întunecat</string>
<string name="pref_value_theme_system">Urmăriți sistemul</string>
<!-- Constants (Do not translate) -->
<!-- Enums and Lists -->
<string name="repeat_no">Fără repetare</string>
<string name="repeat_day">Zilnic</string>
<string name="repeat_week">Săptămânal</string>
<string name="repeat_fortnight">Fortăreaţă</string>
<string name="repeat_month">Lunar</string>
<string name="repeat_year">Anual</string>
<string name="payment_mode_none">Niciunul</string>
<string name="payment_mode_all">Toate</string>
<string name="payment_mode_credit_card">Card de credit</string>
<string name="payment_mode_cash">Numerar</string>
<string name="payment_mode_check">Verifică</string>
<string name="payment_mode_online">Online</string>
<string name="payment_mode_transfer">Transfer</string>
<string name="category_none">Niciunul</string>
<string name="category_all">Toate</string>
<string name="category_all_except_reimbursement">Toate cu excepția rambursării</string>
<string name="category_groceries">Mărfuri</string>
<string name="category_leisure">Barie/petrecere</string>
<string name="category_rent">Chirie</string>
<string name="category_bills">Factură</string>
<string name="category_excursion">Excursie/Cultură</string>
<string name="category_health">Sănătate</string>
<string name="category_shopping">Cumpărături</string>
<string name="category_reimbursement">Rambursare</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_accomodation">Cazare</string>
<string name="category_transport">Transportul</string>
<string name="category_sport">Sport</string>
<!-- Project specific -->
<string name="new_project_action">Ce</string>
<string name="new_project_where">Unde</string>
<string name="where_local">Doar local</string>
<string name="where_cospend">Cocheltuiește</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Alătură-te proiectului existent</string>
<string name="todo_create">Creează un proiect nou</string>
<string name="import_tooltip">Importă din fișier</string>
<string name="choose_account_project_dialog_title">Alege proiect</string>
<string name="choose_account_project_dialog_impossible">Nu au fost găsite proiecte pe acest cont.</string>
<string name="choose_project_management_action">Proiect</string>
<string name="project_added_success">Proiect adăugat cu succes.</string>
<string name="no_projects_text">Încă nu aveți proiecte.</string>
<string name="configure_account_choice">Configurare cont Nextcloud</string>
<string name="add_project_choice">Adăugați proiectul manual</string>
<string name="no_members_text">Niciun membru în acest proiect.</string>
<string name="no_bills_text">Nicio factură găsită.</string>
<string name="member_already_exists">Membrul există deja.</string>
<string name="activity_dialog_title">Proiect: %1$s</string>
<string name="remove_project_confirmation">Proiectul %1$s a fost eliminat.</string>
<string name="file_saved_success">Fișier salvat: %1$s</string>
<string name="import_error_header">Importul a eșuat la rândul %d</string>
<string name="import_error_date">Format de dată nevalid la rândul %d</string>
<string name="import_error_owers">Ceară nevalidă la rândul %d</string>
<string name="add_member_dialog_title">Adaugă Membru</string>
<string name="edit_member_dialog_title">Editează Membru</string>
<string name="member_edit_delete">Ștergere</string>
<string name="project_edition_no_change">Nicio modificare de salvat.</string>
<!-- Settlement -->
<string name="center_none">Niciunul (Optim)</string>
<string name="settle_who">Cine plătește</string>
<string name="settle_to_whom">Cui îi este</string>
<string name="settle_how_much">Sumă</string>
<string name="simple_settle_share">Distribuie</string>
<string name="simple_create_bills">Creați facturi</string>
<string name="settle_bill_what">Decontare</string>
<!-- Currencies -->
<string name="currency_dialog_title">Alege moneda (%s)</string>
<string name="setting_none">Niciunul</string>
<string name="setting_all">Toate</string>
<string name="currency_saved_success">Setări valută salvate.</string>
<string name="main_currency">Moneda principală</string>
<!-- Statistics -->
<string name="label_bills_suggested">Categorii sugerate</string>
<string name="label_bills_skip">Omitere</string>
<string name="stats_date_min">De la</string>
<string name="stats_date_max">La</string>
<string name="stats_who">Membru</string>
<string name="stats_paid">Plătit</string>
<string name="stats_spent">Cheltuit</string>
<string name="stats_balance">Sold</string>
<string name="total">Total: %1$s</string>
<!-- Errors Extra -->
<string name="error_project_connect_check">Conexiune eșuată: %1$s</string>
<string name="error_create_remote_project_helper">Crearea a eșuat: %1$s</string>
<string name="error_edit_remote_project_helper">Eroare la actualizarea proiectului la distanță: %1$s</string>
<string name="remote_project_operation_no_network">Rețea indisponibilă pentru operarea la distanță.</string>
<string name="error_scanning_bill_qr_code">Nu s-a putut analiza codul QR.</string>
<string name="error_token_mismatch">Nepotrivire simbol de autentificare. Vă rugăm să vă autentificați din nou.</string>
<string name="insufficient_access_level">Nu aveţi permisiunea de a efectua această acţiune.</string>
<string name="delete_label_confirmation_title">Ștergere etichetă</string>
<string name="delete_label_confirmation_message">Sunteţi sigur că doriţi să ştergeţi această etichetă?</string>
<!-- About -->
<string name="about_version">Versiunea %1$s</string>
<string name="about_maintainer_title">Responsabil</string>
<string name="about_license_title">Licenta</string>
<string name="about_source_title">Cod sursă</string>
<!-- New constants for backward compatibility or shared use -->
<string name="share_intent_title">Proiectul %1$s</string>
<string name="share_chooser_title">Distribuie %1$s</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More