16 Commits
22 changed files with 608 additions and 468 deletions
+9
View File
@@ -49,12 +49,21 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Run unit tests
run: ./gradlew testDebugUnitTest
- name: Build APK
run: |
VERSION_CODE=$(git rev-list --count HEAD)
VERSION_NAME=$(git describe --tags --always)
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: Cowspent-APK
path: app/build/outputs/apk/signedRelease/app-signedRelease.apk
- name: Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
+43 -10
View File
@@ -1,7 +1,8 @@
plugins {
id 'com.android.application' version '9.2.1'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.0'
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.0'
id 'com.android.application' version '9.4.0'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10'
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10'
id 'jacoco'
}
android {
@@ -40,6 +41,7 @@ android {
debuggable true
initWith(buildTypes.release)
signingConfig = signingConfigs.debug
enableUnitTestCoverage true
}
release {
minifyEnabled true
@@ -69,6 +71,12 @@ android {
compose = true
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
kotlin {
jvmToolchain(21)
}
@@ -96,21 +104,21 @@ android {
}
dependencies {
implementation 'androidx.compose.foundation:foundation:1.11.4'
implementation 'androidx.compose.runtime:runtime:1.11.4'
implementation 'androidx.compose.foundation:foundation:1.12.0'
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'
implementation 'androidx.work:work-runtime-ktx:2.11.2'
implementation 'com.google.zxing:core:3.5.4'
implementation 'androidx.camera:camera-camera2:1.6.1'
implementation 'androidx.camera:camera-lifecycle:1.6.1'
implementation 'androidx.camera:camera-view:1.6.1'
implementation 'androidx.camera:camera-camera2:1.6.2'
implementation 'androidx.camera:camera-lifecycle:1.6.2'
implementation 'androidx.camera:camera-view:1.6.2'
implementation 'com.github.nextcloud:Android-SingleSignOn:1.1.0'
implementation 'com.opencsv:opencsv:5.12.0'
implementation platform('androidx.compose:compose-bom:2026.06.01')
implementation platform('androidx.compose:compose-bom:2026.08.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material:material'
implementation 'androidx.compose.material:material-icons-extended'
@@ -121,5 +129,30 @@ dependencies {
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"
implementation "com.google.crypto.tink:tink-android:1.23.0"
testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.14.11'
testImplementation 'org.robolectric:robolectric:4.16.1'
testImplementation 'androidx.test:core:1.7.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0'
}
tasks.register('jacocoTestReport', JacocoReport) {
dependsOn 'testDebugUnitTest'
reports {
xml.required = true
html.required = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*']
def debugTree = fileTree(dir: "${project.layout.buildDirectory.asFile.get()}/intermediates/built_in_kotlinc/debug/compileDebugKotlin/classes", excludes: fileFilter)
def mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories.from = files([mainSrc])
classDirectories.from = files([debugTree])
executionData.from = fileTree(dir: project.layout.buildDirectory.asFile.get(), includes: [
'jacoco/testDebugUnitTest.exec', 'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec'
])
}
@@ -151,8 +151,9 @@ class EditBillActivity : AppCompatActivity() {
val billIdToDuplicate = intent.getLongExtra(PARAM_BILL_ID_TO_DUPLICATE, 0)
val timeNowSeconds = System.currentTimeMillis() / 1000
if (billIdToDuplicate == 0L) {
val project = db.getProject(projectId)
bill = DBBill(
0, 0, projectId, 0, 0.0, timeNowSeconds,
0, 0, projectId, project?.lastPayerId ?: 0, 0.0, timeNowSeconds,
"", DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, "", DBBill.PAYMODE_ID_NONE
)
@@ -324,15 +325,25 @@ class EditBillActivity : AppCompatActivity() {
private suspend fun saveBill(): Long = withContext(Dispatchers.IO) {
val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS)
val isCustomSplit = viewModel.isCustomSplit
val splitMode = viewModel.splitMode
if (isCustomSplit) {
val splits: Map<Long, Double> = viewModel.owersCustomSplit.filter { (id, amountStr) ->
viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull()
?: 0.0) > 0
}.mapValues {
val uiAmount = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
if (splitMode != SplitMode.EVEN) {
val splits: Map<Long, Double> = if (splitMode == SplitMode.CUSTOM) {
viewModel.owersCustomSplit.filter { (id, amountStr) ->
viewModel.owersSelection[id] == true && viewModel.parseAmountFromUi(amountStr) > 0
}.mapValues {
val uiAmount = viewModel.parseAmountFromUi(it.value)
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
}
} else { // PERCENT
val totalAmount = viewModel.amountAsDouble
viewModel.owersPercentSplit.filter { (id, percentStr) ->
viewModel.owersSelection[id] == true && viewModel.parseAmountFromUi(percentStr) > 0
}.mapValues {
val percent = viewModel.parseAmountFromUi(it.value)
val uiAmount = SupportUtil.round2(totalAmount * percent / 100.0)
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
}
}
if (splits.isEmpty()) return@withContext 0L
@@ -405,16 +416,8 @@ class EditBillActivity : AppCompatActivity() {
val proj = db.getProject(bill.projectId)
if (proj != null) db.syncIfRemote(proj)
db.updateProject(
bill.projectId,
null,
null,
null,
viewModel.payerId,
null,
null,
null,
null,
null
projId = bill.projectId,
newLastPayerId = viewModel.payerId
)
return@withContext firstSavedId
@@ -458,16 +461,8 @@ class EditBillActivity : AppCompatActivity() {
newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) }
val newBillId = db.addBill(newBill)
db.updateProject(
bill.projectId,
null,
null,
null,
viewModel.payerId,
null,
null,
null,
null,
null
projId = bill.projectId,
newLastPayerId = viewModel.payerId
)
val proj = db.getProject(bill.projectId)
if (proj != null) db.syncIfRemote(proj)
@@ -1,11 +1,13 @@
package net.helcel.cowspent.android.bill_edit
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
@@ -16,12 +18,18 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
@@ -53,6 +61,13 @@ fun EditBillScreen(
) {
val canEdit = accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN || accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if (viewModel.isNewBill) {
focusRequester.requestFocus()
}
}
StatefulAlertDialog(
state = viewModel.dialogState,
@@ -128,7 +143,8 @@ fun EditBillScreen(
viewModel = viewModel,
canEdit = canEdit,
onDateClick = onDateClick,
onTimeClick = onTimeClick
onTimeClick = onTimeClick,
amountFocusRequester = focusRequester
)
Spacer(modifier = Modifier.height(8.dp))
@@ -164,7 +180,8 @@ fun BillBasicInfoSection(
viewModel: EditBillViewModel,
canEdit: Boolean,
onDateClick: () -> Unit,
onTimeClick: () -> Unit
onTimeClick: () -> Unit,
amountFocusRequester: FocusRequester
) {
Text(
text = "GENERAL",
@@ -175,29 +192,20 @@ fun BillBasicInfoSection(
)
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val currencyDialogTitle =
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
OutlinedTextField(
value = viewModel.what,
onValueChange = { viewModel.what = it },
enabled = canEdit,
placeholder = { Text(stringResource(R.string.label_what)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.amount,
onValueChange = {
viewModel.amount = it
onValueChange = { nv->
val filteredValue = nv.filter { it in "0123456789.+-*/" }
viewModel.amount = filteredValue
viewModel.updateSplits()
},
enabled = canEdit,
placeholder = { Text("0") },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().focusRequester(amountFocusRequester),
leadingIcon = {
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
viewModel.mainCurrencyName.ifEmpty { "$" }
@@ -231,7 +239,21 @@ fun BillBasicInfoSection(
Icon(Icons.Default.SwapHoriz, contentDescription = "Change Currency")
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone, imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.what,
onValueChange = { viewModel.what = it },
enabled = canEdit,
placeholder = { Text(stringResource(R.string.label_what)) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
)
Spacer(modifier = Modifier.height(8.dp))
@@ -357,34 +379,54 @@ fun OwerSelectionSection(
)
Spacer(Modifier.weight(1f))
if (viewModel.isCustomSplit) {
if (viewModel.splitMode != SplitMode.EVEN) {
val diff = viewModel.getDiffSplit()
if (abs(diff) > 0.01) {
val unit = if (viewModel.splitMode == SplitMode.PERCENT) "%" else ""
val diffText =
if (diff > 0) "Missing: ${SupportUtil.normalNumberFormat.format(diff)}" else "Excess: ${
if (diff > 0) "Missing: ${SupportUtil.normalNumberFormat.format(diff)}$unit" else "Excess: ${
SupportUtil.normalNumberFormat.format(-diff)
}"
}$unit"
Text(
diffText,
color = MaterialTheme.colors.error,
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier.padding(horizontal = 4.dp)
)
}
} else {
Text("Even Split", fontSize = 12.sp)
}
Switch(
checked = !viewModel.isCustomSplit,
enabled = canEdit,
onCheckedChange = {
viewModel.isCustomSplit = !it
viewModel.updateSplits()
},
colors = SwitchDefaults.colors(
uncheckedThumbColor = MaterialTheme.colors.onSurface,
)
)
val splitOptions = listOf(SplitMode.EVEN, SplitMode.CUSTOM, SplitMode.PERCENT)
Row(
modifier = Modifier
.padding(start = 8.dp)
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f), MaterialTheme.shapes.small)
) {
splitOptions.forEach { mode ->
val isSelected = viewModel.splitMode == mode
Box(
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.background(if (isSelected) MaterialTheme.colors.primary else Color.Transparent)
.clickable(enabled = canEdit) {
viewModel.splitMode = mode
viewModel.updateSplits()
}
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = when (mode) {
SplitMode.EVEN -> "="
SplitMode.CUSTOM -> "#"
SplitMode.PERCENT -> "%"
},
color = if (isSelected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
viewModel.members.forEach { member ->
@@ -410,21 +452,34 @@ fun OwerSelectionSection(
Spacer(modifier = Modifier.width(8.dp))
Text(member.name, modifier = Modifier.weight(1f))
if (isSelected || viewModel.isCustomSplit) {
if (isSelected || viewModel.splitMode != SplitMode.EVEN) {
val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
val value = if (viewModel.splitMode == SplitMode.PERCENT) {
viewModel.owersPercentSplit[member.id] ?: ""
} else {
viewModel.owersCustomSplit[member.id] ?: ""
}
BasicTextField(
value = viewModel.owersCustomSplit[member.id] ?: "",
onValueChange = {
viewModel.owersCustomSplit[member.id] = it
viewModel.owersSelection[member.id] = (it != "")
value = value,
onValueChange = { nv ->
val filteredValue = nv.filter { it in "0123456789.+-*/" }
if (viewModel.splitMode == SplitMode.PERCENT) {
viewModel.owersPercentSplit[member.id] = filteredValue
} else {
viewModel.owersCustomSplit[member.id] = filteredValue
}
viewModel.owersSelection[member.id] = (filteredValue != "")
},
modifier = Modifier
.width(80.dp)
.height(46.dp),
interactionSource = interactionSource,
singleLine = true,
enabled = viewModel.isCustomSplit && canEdit,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
enabled = viewModel.splitMode != SplitMode.EVEN && canEdit,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
cursorBrush = SolidColor(MaterialTheme.colors.onSurface),
textStyle = LocalTextStyle.current.copy(
textAlign = TextAlign.Right,
@@ -432,13 +487,13 @@ fun OwerSelectionSection(
),
) { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = viewModel.owersCustomSplit[member.id] ?: "",
value = value,
visualTransformation = VisualTransformation.None,
innerTextField = innerTextField,
singleLine = true,
enabled = viewModel.isCustomSplit && canEdit,
enabled = viewModel.splitMode != SplitMode.EVEN && canEdit,
interactionSource = interactionSource,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.onSurface,
backgroundColor = Color.Transparent,
@@ -16,6 +16,10 @@ import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath
enum class SplitMode {
EVEN, CUSTOM, PERCENT
}
class EditBillViewModel : ViewModel() {
var what by mutableStateOf("")
var amount by mutableStateOf("")
@@ -34,27 +38,36 @@ class EditBillViewModel : ViewModel() {
var members by mutableStateOf<List<DBMember>>(emptyList())
var owersSelection = mutableStateMapOf<Long, Boolean>()
var isCustomSplit by mutableStateOf(false)
var splitMode by mutableStateOf(SplitMode.EVEN)
var owersCustomSplit = mutableStateMapOf<Long, String>()
var owersPercentSplit = mutableStateMapOf<Long, String>()
var dialogState by mutableStateOf<DialogState?>(null)
val amountAsDouble: Double
get() {
return parseAmount(amount) ?: try {
evalMath(amount.replace(',', '.'))
} catch (_: Exception) {
0.0
}
fun parseAmountFromUi(input: String): Double {
if (input.isBlank()) return 0.0
val sanitized = input.replace(',', '.')
// If it contains any math operator, try evalMath first
if (sanitized.any { it in "+-*/" }) {
try {
val result = evalMath(sanitized)
if (result != 0.0) return SupportUtil.round2(result)
} catch (_: Exception) {}
}
val parsed = parseAmount(input) ?: 0.0
return SupportUtil.round2(parsed)
}
val amountAsDouble: Double
get() = parseAmountFromUi(amount)
fun getEvenSplit(): Double {
val selectedOwersCount = owersSelection.count { it.value }
return if (selectedOwersCount > 0) amountAsDouble / selectedOwersCount else 0.0
return if (selectedOwersCount > 0) SupportUtil.round2(amountAsDouble / selectedOwersCount) else 0.0
}
fun updateSplits() {
if (!isCustomSplit) {
if (splitMode == SplitMode.EVEN) {
val even = getEvenSplit()
val evenStr = if (even == 0.0) "" else SupportUtil.round2(even).toString()
members.forEach { m ->
@@ -64,18 +77,36 @@ class EditBillViewModel : ViewModel() {
owersCustomSplit.remove(m.id)
}
}
} else if (splitMode == SplitMode.PERCENT) {
val selectedCount = owersSelection.count { it.value }
if (selectedCount > 0) {
val evenPercent = 100.0 / selectedCount
val evenPercentStr = SupportUtil.round2(evenPercent).toString()
members.forEach { m ->
if (owersSelection[m.id] == true) {
if (owersPercentSplit[m.id].isNullOrEmpty()) {
owersPercentSplit[m.id] = evenPercentStr
}
} else {
owersPercentSplit.remove(m.id)
}
}
}
}
}
fun toggleMember(id: Long, selected: Boolean) {
owersSelection[id] = selected
if (isCustomSplit) {
if (splitMode != SplitMode.EVEN) {
if (selected) {
if (owersCustomSplit[id].isNullOrEmpty()) {
if (splitMode == SplitMode.CUSTOM && owersCustomSplit[id].isNullOrEmpty()) {
owersCustomSplit[id] = "0"
} else if (splitMode == SplitMode.PERCENT && owersPercentSplit[id].isNullOrEmpty()) {
owersPercentSplit[id] = "0"
}
} else {
owersCustomSplit.remove(id)
owersPercentSplit.remove(id)
}
} else {
updateSplits()
@@ -83,10 +114,17 @@ class EditBillViewModel : ViewModel() {
}
fun getDiffSplit(): Double {
val customTotal = owersCustomSplit.entries
.filter { owersSelection[it.key] == true }
.sumOf { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 }
return amountAsDouble - customTotal
return if (splitMode == SplitMode.PERCENT) {
val customTotal = owersPercentSplit.entries
.filter { owersSelection[it.key] == true }
.sumOf { parseAmountFromUi(it.value) }
SupportUtil.round2(100.0 - customTotal)
} else {
val customTotal = owersCustomSplit.entries
.filter { owersSelection[it.key] == true }
.sumOf { parseAmountFromUi(it.value) }
SupportUtil.round2(amountAsDouble - customTotal)
}
}
fun getOwersIds(): List<Long> {
@@ -204,9 +242,10 @@ class EditBillViewModel : ViewModel() {
owersSelection.clear()
owersCustomSplit.clear()
owersPercentSplit.clear()
if (customSplits != null) {
isCustomSplit = true
splitMode = SplitMode.CUSTOM
for (member in members) {
val selected = customSplits.containsKey(member.id)
owersSelection[member.id] = selected
@@ -222,6 +261,7 @@ class EditBillViewModel : ViewModel() {
}
} else {
// Even split logic
splitMode = SplitMode.EVEN
val billOwerIds = bill.billOwersIds
val selectedCount = billOwerIds.size
@@ -84,9 +84,8 @@ class ManageCurrenciesActivity : AppCompatActivity() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
db!!.updateProject(
selectedProjectID, null, null, null,
null, null, newMainCurrencyName,
null, null, null
projId = selectedProjectID,
newCurrencyName = newMainCurrencyName
)
val project = db!!.getProject(selectedProjectID)
if (project != null) {
@@ -94,7 +93,12 @@ class ManageCurrenciesActivity : AppCompatActivity() {
if (project.type == ProjectType.COSPEND) {
withContext(Dispatchers.Main) {
if (!db!!.cowspentServerSyncHelper
.editRemoteProject(selectedProjectID, project.name, null, null, newMainCurrencyName, editMainCurrencyCallBack)
.editRemoteProject(
projId = selectedProjectID,
newName = project.name,
newMainCurrencyName = newMainCurrencyName,
callback = editMainCurrencyCallBack
)
) {
showToast(this@ManageCurrenciesActivity, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG)
}
@@ -4,8 +4,10 @@ import android.annotation.SuppressLint
import android.content.Intent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -417,6 +419,9 @@ fun BillsListScreen(
is SectionItem -> SectionHeader(item.title)
}
}
item {
Spacer(modifier = Modifier.height(64.dp).fillMaxWidth())
}
}
}
}
@@ -88,6 +88,7 @@ object BillsListUtils {
shareIntent,
context.getString(R.string.title_settle)
)
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooserIntent)
}
@@ -427,19 +427,26 @@ class BillsListViewActivity :
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
)
}
if (!proj.isLocal) {
db.cowspentServerSyncHelper.editRemoteProject(
projId = projectId,
newArchivedTs = newArchivedTs,
callback = object : ICallback {
override fun onFinish() {}
override fun onFinish(result: String, message: String) {
if (message.isNotEmpty()) {
showToast(this@BillsListViewActivity, getString(R.string.error_sync, message))
}
}
override fun onScheduled() {}
}
)
}
setupDrawerProjects()
refreshLists()
@@ -114,7 +114,7 @@ object ProjectImportHelper {
}
else -> 0
}
val payerName = if (columns.containsKey("payer_name")) line[columns["payer_name"]!!] else ""
val payerName = if (columns.containsKey("payer_name")) line[columns["payer_name"]!!].trim() else ""
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"
@@ -122,8 +122,10 @@ object ProjectImportHelper {
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
membersWeight[payerName] = payerWeight
if (payerName.isNotEmpty()) {
membersActive[payerName] = payerActive
membersWeight[payerName] = payerWeight
}
if (owersStr.trim().isEmpty()) {
onError(context.getString(R.string.import_error_owers, row))
@@ -132,10 +134,10 @@ object ProjectImportHelper {
if (what != "deleteMeIfYouWant") {
billRemoteIdToOwerStr[row.toLong()] = owersStr
val owersArray = owersStr.split(", ").filter { it.isNotEmpty() }
val owersArray = owersStr.split(",").map { it.trim() }.filter { it.isNotEmpty() }
for (ower in owersArray) {
if (!membersWeight.containsKey(ower.trim())) {
membersWeight[ower.trim()] = 1.0
if (!membersWeight.containsKey(ower)) {
membersWeight[ower] = 1.0
}
}
bills.add(DBBill(0, row.toLong(), 0, 0, amount, timestamp, what, DBBill.STATE_OK, "n", pm, catId, comment, pmId))
@@ -171,8 +173,8 @@ object ProjectImportHelper {
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) }
billRemoteIdToOwerStr[b.remoteId]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }?.forEach { ower ->
memberNameToId[ower]?.let { owerId -> db.addBillower(billId, owerId) }
}
}
onSuccess(pid)
@@ -85,10 +85,11 @@ class EditProjectActivity : AppCompatActivity() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
db.updateProject(
project.id, newName, newEmail, targetPwd,
null, project.type, null,
null, null,
null, null
projId = project.id,
newName = newName,
newEmail = newEmail,
newPassword = targetPwd,
projectType = project.type
)
}
closeOnEdit(project.id)
@@ -103,18 +104,21 @@ class EditProjectActivity : AppCompatActivity() {
project.password = currentPwd
lifecycleScope.launch {
withContext(Dispatchers.IO) {
db.updateProject(project.id, null, null, currentPwd, null, project.type, null, null, null, null, null)
db.updateProject(
projId = project.id,
newPassword = currentPwd,
projectType = project.type
)
}
}
}
if (!db.cowspentServerSyncHelper.editRemoteProject(
project.id,
newName,
newEmail,
if (pwdChanged) newPwd else null,
null,
editCallBack
projId = project.id,
newName = newName,
newEmail = newEmail,
newPassword = if (pwdChanged) newPwd else null,
callback = editCallBack
)
) {
showToast(this, getString(R.string.remote_project_operation_no_network), Toast.LENGTH_LONG)
@@ -124,10 +128,9 @@ class EditProjectActivity : AppCompatActivity() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
db.updateProject(
project.id, null, null, currentPwd,
null, project.type, null,
null, null,
null, null
projId = project.id,
newPassword = currentPwd,
projectType = project.type
)
}
closeOnEdit(project.id)
@@ -271,12 +271,18 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
}
fun updateProject(
projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newLastPayerId: Long?,
newLastSyncedTimestamp: Long?,
newCurrencyName: String?, newDeletionDisabled: Boolean?,
newMyAccessLevel: Int?, newBearerToken: String?,
newArchivedTs: Long? = null
projId: Long,
newName: String? = null,
newEmail: String? = null,
newPassword: String? = null,
newLastPayerId: Long? = null,
newLastSyncedTimestamp: Long? = null,
newCurrencyName: String? = null,
newDeletionDisabled: Boolean? = null,
newMyAccessLevel: Int? = null,
newBearerToken: String? = null,
newArchivedTs: Long? = null,
projectType: ProjectType? = null
) {
val db = writableDatabase
val values = ContentValues()
@@ -290,47 +296,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
if (newDeletionDisabled != null) values.put(key_deletionDisabled, if (newDeletionDisabled) 1 else 0)
if (newMyAccessLevel != null) values.put(key_myAccessLevel, newMyAccessLevel)
if (newArchivedTs != null) values.put(key_archived, newArchivedTs)
if (values.size() > 0) {
db.update(table_projects, values, "$key_id = ?", arrayOf(projId.toString()))
}
}
fun updateProject(
projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newLastPayerId: Long?,
projectType: ProjectType, newLastSyncedTimestamp: Long?,
newCurrencyName: String?, newDeletionDisabled: Boolean?,
newMyAccessLevel: Int?, newBearerToken: String?,
newArchivedTs: Long? = null
) {
val db = writableDatabase
updateProject(
projId, newName, newEmail, newPassword, newLastPayerId, projectType,
newLastSyncedTimestamp, newCurrencyName, newDeletionDisabled, newMyAccessLevel,
newBearerToken, newArchivedTs, db
)
}
private fun updateProject(
projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newLastPayerId: Long?,
projectType: ProjectType, newLastSyncedTimestamp: Long?,
newCurrencyName: String?, newDeletionDisabled: Boolean?,
newMyAccessLevel: Int?, newBearerToken: String?,
newArchivedTs: Long?, db: SQLiteDatabase
) {
val values = ContentValues()
if (newName != null) values.put(key_name, newName)
if (newEmail != null) values.put(key_email, newEmail)
if (newPassword != null) SecureStorage.savePasswordSync(context, "ProjectPassword_$projId", newPassword)
if (newBearerToken != null) values.put(key_bearer_token, newBearerToken)
if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId)
if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp)
if (newCurrencyName != null) values.put(key_currencyName, newCurrencyName)
if (newDeletionDisabled != null) values.put(key_deletionDisabled, if (newDeletionDisabled) 1 else 0)
if (newMyAccessLevel != null) values.put(key_myAccessLevel, newMyAccessLevel)
if (newArchivedTs != null) values.put(key_archived, newArchivedTs)
values.put(key_type, projectType.id)
if (projectType != null) values.put(key_type, projectType.id)
if (values.size() > 0) {
db.update(table_projects, values, "$key_id = ?", arrayOf(projId.toString()))
}
@@ -85,7 +85,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val isSyncPossible: Boolean
get() {
updateNetworkStatus()
return networkConnected
val offlineMode = preferences.getBoolean(appContext.getString(R.string.pref_key_offline_mode), false)
return networkConnected && !offlineMode
}
fun addCallbackPull(callback: ICallback) {
@@ -817,10 +818,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
}
dbHelper.updateProject(
project.id, null, null,
null, null, serverSyncTimestamp,
null, null, null,
null
projId = project.id,
newLastSyncedTimestamp = serverSyncTimestamp
)
LoginStatus.OK
} catch (_: ServerResponse.NotModifiedException) {
@@ -935,11 +934,18 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
return isCospend && projUrl == accountUrl
}
fun editRemoteProject(projId: Long, newName: String?, newEmail: String?,
newPassword: String?, newMainCurrencyName: String?, callback: ICallback): Boolean {
fun editRemoteProject(
projId: Long,
newName: String? = null,
newEmail: String? = null,
newPassword: String? = null,
newMainCurrencyName: String? = null,
newArchivedTs: Long? = null,
callback: ICallback
): Boolean {
updateNetworkStatus()
if (isSyncPossible) {
EditRemoteProjectTask(projId, newName, newEmail, newPassword, newMainCurrencyName, callback).execute()
EditRemoteProjectTask(projId, newName, newEmail, newPassword, newMainCurrencyName, newArchivedTs, callback).execute()
return true
}
return false
@@ -951,6 +957,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
private val newEmail: String?,
private val newPassword: String?,
private val newMainCurrencyName: String?,
private val newArchivedTs: Long?,
private val callback: ICallback
) {
private val project: DBProject? = dbHelper.getProject(projId)
@@ -985,7 +992,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
var status = LoginStatus.OK
try {
val response = client!!.editRemoteProject(
project!!, newName, newEmail, newPassword, newMainCurrencyName
project!!, newName, newEmail, newPassword, newMainCurrencyName, newArchivedTs
)
if (BillsListViewActivity.DEBUG) {
Log.i(TAG, "RESPONSE edit remote project : ${response.stringContent}")
@@ -1025,8 +1032,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
}
} else {
dbHelper.updateProject(
project!!.id, newName, newEmail, newPassword,
null, null, null, null, null, null
projId = project!!.id,
newName = newName,
newEmail = newEmail,
newPassword = newPassword,
newArchivedTs = newArchivedTs
)
}
callback.onFinish(newName ?: "", errorString)
@@ -1205,7 +1215,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
}
}
private fun hasChanged(localBill: DBBill, remoteBill: DBBill): Boolean {
internal fun hasChanged(localBill: DBBill, remoteBill: DBBill): Boolean {
if (localBill.payerId == remoteBill.payerId &&
localBill.amount == remoteBill.amount &&
localBill.timestamp == remoteBill.timestamp &&
@@ -114,7 +114,7 @@ class VersatileProjectSyncClient(
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemoteProject(
project: DBProject, newName: String?, newEmail: String?, newPassword: String?,
newMainCurrencyName: String?
newMainCurrencyName: String?, newArchivedTs: Long? = null
): ServerResponse.EditRemoteProjectResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
@@ -130,6 +130,10 @@ class VersatileProjectSyncClient(
paramKeys.add("password")
paramValues.add(newPassword)
}
if (newArchivedTs != null) {
paramKeys.add("archived_ts")
paramValues.add(newArchivedTs.toString())
}
var target: String
var username: String? = null
@@ -5,9 +5,11 @@ fun evalMath(expression: String): Double {
var result = 0.0
var db: SQLiteDatabase? = null
try {
// Force floating point division by replacing / with * 1.0 /
val forcedRealExpr = expression.replace("/", "* 1.0 /")
// Opens a temporary, in-memory system database block
db = SQLiteDatabase.create(null)
val cursor = db.rawQuery("SELECT ($expression);", null)
val cursor = db.rawQuery("SELECT ($forcedRealExpr);", null)
if (cursor.moveToFirst()) {
result = cursor.getDouble(0)
}
+205 -205
View File
@@ -2,240 +2,240 @@
<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_new_bill">Nuevo gasto</string>
<string name="action_add_project">Agregar 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">Borrar</string>
<string name="action_archive">Archivar</string>
<string name="action_unarchive">Desarchivar</string>
<string name="action_export">Exportar</string>
<string name="action_stats">Stadísticas</string>
<string name="action_settle">Saldar</string>
<string name="action_scan_qrcode">Escanear QR</string>
<string name="action_settings">Configuraciones</string>
<string name="action_label_bills">Categorías sin etiquetas</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>
<string name="action_connect">Conectar</string>
<string name="action_discard">Discartar</string>
<string name="action_members">Miembros</string>
<string name="action_labels">Etiquetas</string>
<string name="action_currencies">Moneda</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>
<string name="title_stats">Estadísticas</string>
<string name="title_edit_project">Editar Proyecto</string>
<string name="title_label_bills">Etiquetar gastos</string>
<string name="title_labels">Administrar etiquetas</string>
<string name="title_about">Acerca de</string>
<string name="title_settle">Saldar Proyecto</string>
<string name="title_share">Compartir Proyecto</string>
<string name="title_add_project">Agregar Proyecto</string>
<string name="title_add_category">Agregar Categoría</string>
<string name="title_add_payment_mode">Agregar Modo de Pago</string>
<string name="title_account">Cuenta Nextcloud</string>
<string name="title_share_web">Enlace Web</string>
<string name="title_share_qr">Enlace Cowspent</string>
<string name="title_confirm">Estás seguro?</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_all_bills">Todos los gastos</string>
<string name="label_categories">Categorias</string>
<string name="label_payment_modes">Modos de Pago</string>
<string name="label_name">Nombre</string>
<string name="label_icon">Icono / 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_weight">Ponderación</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">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>
<string name="label_url">Dirección del Servidor</string>
<string name="label_username">Nombre de Usuario</string>
<string name="label_comment">Comentario</string>
<string name="label_what">Qué?</string>
<string name="label_payer">Quién paga?</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 Proyecto/Nombre</string>
<string name="label_project_title">Título Proyecto</string>
<string name="label_use_sso">Usar cuenta de Nextcloud App</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>
<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 será eliminado.</string>
<string name="dialog_sync_error_title">Error de sincronización</string>
<string name="dialog_sync_error_msg">Falla de sincronización para %1$s.\n\n%2$s</string>
<string name="dialog_balanced_msg">Gastos ya balanceados.</string>
<string name="msg_project_added">Agregado proyecto %1$s</string>
<string name="msg_bill_labeled_done">Todos los gastos etiquetados</string>
<string name="msg_no_suggestions">Sin sugerencias</string>
<string name="msg_auth_warning">Se requiere Cospend v0.3.4+.</string>
<string name="msg_link_copied">Vínculo copiado al portapapeles</string>
<string name="msg_share_qr">Escanea el QR o comparte el vínculo para unirte.</string>
<string name="msg_share_web">Vínculo para acceso mediante navegador web.</string>
<string name="msg_share_qr_warn">Comparte este vínculo con otro usuario de Cowspent.</string>
<string name="msg_settle_intro">Liquidación 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 para %1$s:</string>
<string name="msg_stats_header">Miembro (Pagado | Gastado | Balance)</string>
<string name="msg_logged_in_as">Conectado como %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_loading">Cargando</string>
<string name="error_no_projects">No se encontraron proyectos</string>
<string name="error_no_members">No se encontraron miembros</string>
<string name="error_no_bills">No se encontraron gastos</string>
<string name="error_no_member">Se requiere al menos un miembro</string>
<string name="error_maintenance_mode">Servidor en modo mantenimiento</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>
<string name="error_sync">Fallo en sicronización: %1$s</string>
<string name="error_invalid_login">Inicio sesión inválida: %1$s</string>
<string name="error_auth">Usuario o contraseña incorrectos</string>
<string name="error_json">Respuesta inválida del servidor</string>
<string name="error_req_failed">Falla en solicitud</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 de proyecto inválido</string>
<string name="error_invalid_bill_name">Nombre de gasto inválido</string>
<string name="error_invalid_bill_date">Fecha de gasto inválido</string>
<string name="error_invalid_bill_payer">Se requiere pagador</string>
<string name="error_invalid_bill_owers">Se requiere deudor</string>
<string name="error_no_network">Sin conexión de red</string>
<string name="error_server">Error de servidor</string>
<string name="error_io">Conexión de servidor interrumpida</string>
<string name="error_share_impossible">No se puede compartir este proyecto</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="drawer_no_account">Conectar a cuenta Nextcloud</string>
<string name="drawer_last_sync">Ultima 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">Yes</string>
<string name="simple_yes">Si</string>
<string name="simple_no">No</string>
<string name="simple_close">Close</string>
<string name="simple_close">Cerrar</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="settings_appearance">Apariencia</string>
<string name="settings_network">Red</string>
<string name="settings_other">Otros</string>
<string name="settings_night_mode">Tema</string>
<string name="settings_offline_mode">Modo fuera de línea</string>
<string name="settings_offline_mode_summary">Solo sincronización manual.</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">Funciones beta</string>
<string name="settings_beta_features_summary">Habilitar funciones beta. Usar bajo tu propia responsabilidad.</string>
<string name="settings_url_warn_http">ADVERTENCIA: \"http\" es inseguro. Usa \"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">Light</string>
<string name="pref_value_theme_dark">Dark</string>
<string name="pref_value_theme_system">Follow system</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 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>
<string name="repeat_no">No repetir</string>
<string name="repeat_day">Diario</string>
<string name="repeat_week">Semanal</string>
<string name="repeat_fortnight">Quincenal</string>
<string name="repeat_month">Mensual</string>
<string name="repeat_year">Anual</string>
<string name="payment_mode_none">Ninguno</string>
<string name="payment_mode_all">Todos</string>
<string name="payment_mode_credit_card">Tarjeta de crédito</string>
<string name="payment_mode_cash">Efectivo</string>
<string name="payment_mode_check">Cheque</string>
<string name="payment_mode_online">En línea</string>
<string name="payment_mode_transfer">Transferencia</string>
<string name="category_none">Ninguna</string>
<string name="category_all">Todas</string>
<string name="category_all_except_reimbursement">Todas excepto reembolsos</string>
<string name="category_groceries">Supermercado</string>
<string name="category_leisure">Bar/Fiesta</string>
<string name="category_rent">Renta</string>
<string name="category_bills">Facturas/Servicios</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">Reintegro</string>
<string name="category_restaurant">Restaurante</string>
<string name="category_accomodation">Alojamiento</string>
<string name="category_transport">Transporte</string>
<string name="category_sport">Deportes</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="new_project_action">Qué</string>
<string name="new_project_where">Cuando</string>
<string name="where_local">Solo local</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>
<string name="todo_join">Unirse a 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 projecto</string>
<string name="choose_account_project_dialog_impossible">No se encotraron proyectos en esta cuenta.</string>
<string name="choose_project_management_action">Proyecto</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 gastos.</string>
<string name="member_already_exists">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">Importación fallida en línea %d</string>
<string name="import_error_date">Formato de fecha inválido en línea %d</string>
<string name="import_error_owers">Deudores inválidos en línea %d</string>
<string name="add_member_dialog_title">Agregar Miembro</string>
<string name="edit_member_dialog_title">Editar Miembro</string>
<string name="member_edit_delete">Eliminar</string>
<string name="project_edition_no_change">Sin cambios para guardar.</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>
<string name="center_none">Ninguno (Optimo)</string>
<string name="settle_who">Quién paga</string>
<string name="settle_to_whom">A quién</string>
<string name="settle_how_much">Monto</string>
<string name="simple_settle_share">Compartir</string>
<string name="simple_create_bills">Crear gastos</string>
<string name="settle_bill_what">Liquidación</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>
<string name="currency_dialog_title">Elegir moneda (%s)</string>
<string name="setting_none">Ninguna</string>
<string name="setting_all">Todas</string>
<string name="currency_saved_success">Configuración de moneda guardada.</string>
<string name="main_currency">Moneda principal</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="label_bills_suggested">Categorías sugeridas</string>
<string name="label_bills_skip">Saltear</string>
<string name="stats_date_min">Desde</string>
<string name="stats_date_max">Hasta</string>
<string name="stats_who">Miembro</string>
<string name="stats_paid">Pagado</string>
<string name="stats_spent">Gastado</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>
<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 actualizado proyecto remoto: %1$s</string>
<string name="remote_project_operation_no_network">Red no disponible para operación remota.</string>
<string name="error_scanning_bill_qr_code">No se pudo interpretar el código QR.</string>
<string name="error_token_mismatch">El token de autenticación no coincide. 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">Seguro quieres eliminar la etiqueta?</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>
<string name="about_version">Versión %1$s</string>
<string name="about_maintainer_title">Responsable</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">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
<string name="share_intent_title">Proyecto %1$s</string>
<string name="share_chooser_title">Compartir %1$s</string>
</resources>
+49 -33
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Kuh ausgegeben</string>
<string name="app_name">Cowspent</string>
<!-- Actions -->
<string name="action_new_bill">Neue Rechnung</string>
<string name="action_add_project">Projekt hinzufügen</string>
@@ -10,38 +12,40 @@
<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_unarchive">Reaktivieren</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_scan_qrcode">QR Code einscannen</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_labels">Labels</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_label_bills">Rechnungen kennzeichnen</string>
<string name="title_labels">Labels verwalten</string>
<string name="title_about">Über</string>
<string name="title_settle">Projekt regeln</string>
<string name="title_settle">Projekt abrechnen</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_add_payment_mode">Zahlungsmethode hinzufügen</string>
<string name="title_account">Nextloud-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_payment_modes">Zahlungsmethoden</string>
<string name="label_name">Name</string>
<string name="label_icon">Icon / Emoji</string>
<string name="label_color">Farbe</string>
@@ -61,26 +65,28 @@
<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_confirm_remove_project_msg">Das Remote-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_bill_labeled_done">Alle Rechnungen kategorisiert</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_share_qr">Scanne den QR-Code oder teile den Link, um beizutreten.</string>
<string name="msg_share_web">Link für den Zugriff per Web-Browser.</string>
<string name="msg_share_qr_warn">Teile 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_settle_sentence">%1$s schuldet %3$.2f an %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_stats_header">Mitglied (Gezahlt | Ausgegeben | Saldo)</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>
@@ -104,11 +110,12 @@
<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_invalid_bill_owers">Schuldner 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_io">Serververbindung getrennt</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>
@@ -117,6 +124,7 @@
<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>
@@ -129,20 +137,21 @@
<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_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwende \"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_server">Nextcloud</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_fortnight">Vierzehntägig</string>
<string name="repeat_month">Monatlich</string>
<string name="repeat_year">Jährlich</string>
<string name="payment_mode_none">Keine</string>
@@ -154,33 +163,34 @@
<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_all_except_reimbursement">Alle außer Erstattung</string>
<string name="category_groceries">Lebensmittel</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_reimbursement">Erstattung</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_cospend">Cospend</string>
<string name="where_ihatemoney">IHateMoney</string>
<string name="todo_join">Bestehendes Projekt beitreten</string>
<string name="todo_join">Bestehendem 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="no_projects_text">Du hast 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>
@@ -191,11 +201,12 @@
<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="import_error_owers">Ungültige Schuldner 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>
@@ -204,12 +215,14 @@
<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>
@@ -220,21 +233,24 @@
<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="error_edit_remote_project_helper">Fehler beim Aktualisieren des Remote-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>
<string name="error_token_mismatch">Authentifizierungstoken stimmen nicht überein. Bitte melde dich erneut an.</string>
<string name="insufficient_access_level">Du hast keine Berechtigung, um diese Aktion durchzuführen.</string>
<string name="delete_label_confirmation_title">Kategorie löschen</string>
<string name="delete_label_confirmation_message">Bist du sicher, dass du diese Kategorie löschen möchtest?</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>
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Vendored
+5 -8
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+12 -24
View File
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,30 +65,18 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+3
View File
@@ -6,6 +6,9 @@ pluginManagement {
maven { url = 'https://jitpack.io' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {