diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt index 617c9a5..fda7269 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt @@ -51,7 +51,7 @@ 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.StatefulAlertDialog import net.helcel.cowspent.android.helper.formatAmount import net.helcel.cowspent.model.DBCurrency import kotlin.time.Duration.Companion.milliseconds @@ -66,35 +66,10 @@ fun ManageCurrenciesScreen( onEdit: (DBCurrency) -> Unit, onCancelEdit: () -> Unit ) { - val dialogState = viewModel.dialogState - if (dialogState != null) { - AlertDialog( - showDialog = true, - onDismissRequest = { viewModel.dismissDialog() }, - title = dialogState.title, - message = dialogState.message, - icon = dialogState.icon, - items = dialogState.items, - positiveText = dialogState.positiveText, - negativeText = dialogState.negativeText, - neutralText = dialogState.neutralText, - onConfirm = { - dialogState.onConfirm?.invoke() - viewModel.dismissDialog() - }, - onCancel = { - dialogState.onCancel?.invoke() - viewModel.dismissDialog() - }, - onNeutral = { - dialogState.onNeutral?.invoke() - viewModel.dismissDialog() - } - ) { - dialogState.onItemSelected?.invoke(it) - viewModel.dismissDialog() - } - } + StatefulAlertDialog( + state = viewModel.dialogState, + onDismissRequest = { viewModel.dismissDialog() } + ) Scaffold( topBar = { diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt b/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt index d465e58..2fcf85e 100644 --- a/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt +++ b/app/src/main/java/net/helcel/cowspent/android/helper/TextDrawable.kt @@ -1,7 +1,6 @@ package net.helcel.cowspent.android.helper -import android.graphics.* -import android.graphics.drawable.Drawable +import android.graphics.Color import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* @@ -11,209 +10,143 @@ import kotlin.math.round import kotlin.math.sqrt /** - * A Drawable object that draws text (1 character) on top of a circular/filled background. + * The color a member's avatar gets, derived from their name. */ -class TextDrawable private constructor( - private val mText: String, - r: Int, - g: Int, - b: Int, - private val mRadius: Float, - private val mDisabled: Boolean -) : Drawable() { - private val mTextPaint: Paint = Paint() - private val mBackground: Paint = Paint() - private val mDisabledCircle: Paint = Paint() +object TextDrawable { + private const val INDEX_RED = 0 + private const val INDEX_GREEN = 1 + private const val INDEX_BLUE = 2 + private const val INDEX_HUE = 0 + private const val INDEX_SATURATION = 1 + private const val INDEX_LUMINATION = 2 - init { - mBackground.style = Paint.Style.FILL - mBackground.isAntiAlias = true - mBackground.color = Color.rgb(r, g, b) - - if ((r + g + b) / 3 < 220) { - mTextPaint.color = Color.WHITE - } else { - mTextPaint.color = Color.BLACK + fun getColorFromName(name: String): Int { + return try { + val hsl = calculateHSL(name) + val rgb = hslToRgb(hsl[0].toFloat(), hsl[1].toFloat(), hsl[2].toFloat(), 1f) + Color.rgb(rgb[0], rgb[1], rgb[2]) + } catch (_: NoSuchAlgorithmException) { + Color.WHITE } - mTextPaint.textSize = mRadius - mTextPaint.isAntiAlias = true - mTextPaint.textAlign = Paint.Align.CENTER - - mDisabledCircle.style = Paint.Style.STROKE - mDisabledCircle.strokeWidth = mRadius * 0.2f - mDisabledCircle.isAntiAlias = true - mDisabledCircle.color = Color.DKGRAY } - override fun draw(canvas: Canvas) { - canvas.drawCircle(mRadius, mRadius, mRadius, mBackground) - canvas.drawText( - mText, - mRadius, - mRadius - (mTextPaint.descent() + mTextPaint.ascent()) / 2, - mTextPaint + @Throws(NoSuchAlgorithmException::class) + private fun calculateHSL(name: String): IntArray { + val result = arrayOf("0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0") + val rgb = doubleArrayOf(0.0, 0.0, 0.0) + var sat = 70 + val lum = 68 + val modulo = 16 + + var hash = name.lowercase(Locale.ROOT).replace("[^0-9a-f]".toRegex(), "") + if (!hash.matches("^[0-9a-f]{32}$".toRegex())) { + hash = md5(hash) + } + + for (i in hash.indices) { + result[i % modulo] = (result[i % modulo].toInt() + hash.substring(i, i + 1).toInt(16)).toString() + } + + for (count in 1 until modulo) { + rgb[count % 3] += result[count].toDouble() + } + + rgb[INDEX_RED] = rgb[INDEX_RED] % 255 + rgb[INDEX_GREEN] = rgb[INDEX_GREEN] % 255 + rgb[INDEX_BLUE] = rgb[INDEX_BLUE] % 255 + + val hsl = rgbToHsl(rgb[INDEX_RED], rgb[INDEX_GREEN], rgb[INDEX_BLUE]) + + val bright = sqrt( + 0.299 * rgb[INDEX_RED].pow(2.0) + 0.587 * rgb[INDEX_GREEN].pow(2.0) + 0.114 * rgb[INDEX_BLUE].pow(2.0) ) - if (mDisabled) { - canvas.drawCircle(mRadius, mRadius, mRadius * 0.9f, mDisabledCircle) - canvas.drawLine( - mRadius * 0.4f, - mRadius * 1.6f, - mRadius * 1.6f, - mRadius * 0.4f, - mDisabledCircle - ) + + if (bright >= 200) { + sat = 60 } + + return intArrayOf((hsl[INDEX_HUE] * 360).toInt(), sat, lum) } - override fun setAlpha(alpha: Int) { - mTextPaint.alpha = alpha + private fun hslToRgb(hParam: Float, sParam: Float, lParam: Float, alpha: Float): IntArray { + var h = hParam + var s = sParam + var l = lParam + if (s !in 0.0f..100.0f) { + throw IllegalArgumentException("Color parameter outside of expected range - Saturation") + } + if (l !in 0.0f..100.0f) { + throw IllegalArgumentException("Color parameter outside of expected range - Luminance") + } + if (alpha !in 0.0f..1.0f) { + throw IllegalArgumentException("Color parameter outside of expected range - Alpha") + } + + h %= 360.0f + h /= 360f + s /= 100f + l /= 100f + + val q = if (l < 0.5) { + l * (1 + s) + } else { + (l + s) - s * l + } + val p = 2 * l - q + val r = round(max(0f, hueToRgb(p, q, h + 1.0f / 3.0f)) * 256).toInt() + val g = round(max(0f, hueToRgb(p, q, h)) * 256).toInt() + val b = round(max(0f, hueToRgb(p, q, h - 1.0f / 3.0f)) * 256).toInt() + return intArrayOf(r, g, b) } - override fun setColorFilter(cf: ColorFilter?) { - mTextPaint.colorFilter = cf + private fun hueToRgb(p: Float, q: Float, hParam: Float): Float { + var h = hParam + if (h < 0) h += 1f + if (h > 1) h -= 1f + if (6 * h < 1) return p + (q - p) * 6 * h + if (2 * h < 1) return q + if (3 * h < 2) return p + (q - p) * 6 * (2.0f / 3.0f - h) + return p } - @Deprecated("Deprecated in Java") - override fun getOpacity(): Int { - return PixelFormat.TRANSLUCENT - } - - companion object { - private const val INDEX_RED = 0 - private const val INDEX_GREEN = 1 - private const val INDEX_BLUE = 2 - private const val INDEX_HUE = 0 - private const val INDEX_SATURATION = 1 - private const val INDEX_LUMINATION = 2 - - fun getColorFromName(name: String): Int { - return try { - val hsl = calculateHSL(name) - val rgb = hslToRgb(hsl[0].toFloat(), hsl[1].toFloat(), hsl[2].toFloat(), 1f) - Color.rgb(rgb[0], rgb[1], rgb[2]) - } catch (_: NoSuchAlgorithmException) { - Color.WHITE - } - } - - @Throws(NoSuchAlgorithmException::class) - private fun calculateHSL(name: String): IntArray { - val result = arrayOf("0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0") - val rgb = doubleArrayOf(0.0, 0.0, 0.0) - var sat = 70 - val lum = 68 - val modulo = 16 - - var hash = name.lowercase(Locale.ROOT).replace("[^0-9a-f]".toRegex(), "") - if (!hash.matches("^[0-9a-f]{32}$".toRegex())) { - hash = md5(hash) - } - - for (i in hash.indices) { - result[i % modulo] = (result[i % modulo].toInt() + hash.substring(i, i + 1).toInt(16)).toString() - } - - for (count in 1 until modulo) { - rgb[count % 3] += result[count].toDouble() - } - - rgb[INDEX_RED] = rgb[INDEX_RED] % 255 - rgb[INDEX_GREEN] = rgb[INDEX_GREEN] % 255 - rgb[INDEX_BLUE] = rgb[INDEX_BLUE] % 255 - - val hsl = rgbToHsl(rgb[INDEX_RED], rgb[INDEX_GREEN], rgb[INDEX_BLUE]) - - val bright = sqrt( - 0.299 * rgb[INDEX_RED].pow(2.0) + 0.587 * rgb[INDEX_GREEN].pow(2.0) + 0.114 * rgb[INDEX_BLUE].pow(2.0) - ) - - if (bright >= 200) { - sat = 60 - } - - return intArrayOf((hsl[INDEX_HUE] * 360).toInt(), sat, lum) - } - - private fun hslToRgb(hParam: Float, sParam: Float, lParam: Float, alpha: Float): IntArray { - var h = hParam - var s = sParam - var l = lParam - if (s !in 0.0f..100.0f) { - throw IllegalArgumentException("Color parameter outside of expected range - Saturation") - } - if (l !in 0.0f..100.0f) { - throw IllegalArgumentException("Color parameter outside of expected range - Luminance") - } - if (alpha !in 0.0f..1.0f) { - throw IllegalArgumentException("Color parameter outside of expected range - Alpha") - } - - h %= 360.0f - h /= 360f - s /= 100f - l /= 100f - - val q = if (l < 0.5) { - l * (1 + s) - } else { - (l + s) - s * l - } - val p = 2 * l - q - val r = round(max(0f, hueToRgb(p, q, h + 1.0f / 3.0f)) * 256).toInt() - val g = round(max(0f, hueToRgb(p, q, h)) * 256).toInt() - val b = round(max(0f, hueToRgb(p, q, h - 1.0f / 3.0f)) * 256).toInt() - return intArrayOf(r, g, b) - } - - private fun hueToRgb(p: Float, q: Float, hParam: Float): Float { - var h = hParam - if (h < 0) h += 1f - if (h > 1) h -= 1f - if (6 * h < 1) return p + (q - p) * 6 * h - if (2 * h < 1) return q - if (3 * h < 2) return p + (q - p) * 6 * (2.0f / 3.0f - h) - return p - } - - private fun rgbToHsl(rUntrimmed: Double, gUntrimmed: Double, bUntrimmed: Double): DoubleArray { - val r = rUntrimmed / 255 - val g = gUntrimmed / 255 - val b = bUntrimmed / 255 - val max = max(r, max(g, b)) - val min = r.coerceAtMost(g.coerceAtMost(b)) - var h = (max + min) / 2 - val s: Double - val l = (max + min) / 2 - if (max == min) { - s = 0.0 - h = s // achromatic - } else { - val d = max - min - s = if (l > 0.5) d / (2 - max - min) else d / (max + min) - when (max) { - r -> { - h = (g - b) / d + (if (g < b) 6 else 0) - } - g -> { - h = (b - r) / d + 2 - } - b -> { - h = (r - g) / d + 4 - } + private fun rgbToHsl(rUntrimmed: Double, gUntrimmed: Double, bUntrimmed: Double): DoubleArray { + val r = rUntrimmed / 255 + val g = gUntrimmed / 255 + val b = bUntrimmed / 255 + val max = max(r, max(g, b)) + val min = r.coerceAtMost(g.coerceAtMost(b)) + var h = (max + min) / 2 + val s: Double + val l = (max + min) / 2 + if (max == min) { + s = 0.0 + h = s // achromatic + } else { + val d = max - min + s = if (l > 0.5) d / (2 - max - min) else d / (max + min) + when (max) { + r -> { + h = (g - b) / d + (if (g < b) 6 else 0) + } + g -> { + h = (b - r) / d + 2 + } + b -> { + h = (r - g) / d + 4 } - h /= 6.0 } - val hsl = DoubleArray(3) - hsl[INDEX_HUE] = h - hsl[INDEX_SATURATION] = s - hsl[INDEX_LUMINATION] = l - return hsl + h /= 6.0 } + val hsl = DoubleArray(3) + hsl[INDEX_HUE] = h + hsl[INDEX_SATURATION] = s + hsl[INDEX_LUMINATION] = l + return hsl + } - @Throws(NoSuchAlgorithmException::class) - private fun md5(string: String): String { - val md5 = MessageDigest.getInstance("MD5").digest(string.toByteArray()) - return md5.joinToString("") { "%02x".format(it) } - } + @Throws(NoSuchAlgorithmException::class) + private fun md5(string: String): String { + val md5 = MessageDigest.getInstance("MD5").digest(string.toByteArray()) + return md5.joinToString("") { "%02x".format(it) } } } diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt index 3fbb5f1..b10c6b1 100644 --- a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt @@ -90,7 +90,7 @@ fun LabelManagementScreenContent( title = { Text(stringResource(R.string.title_labels)) }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.simple_back)) } }, backgroundColor = MaterialTheme.colors.primary, @@ -267,10 +267,10 @@ fun LabelItem( 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)) + Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.action_edit), 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)) + Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.action_delete), tint = MaterialTheme.colors.error.copy(alpha = 0.6f)) } } } diff --git a/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt b/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt index 6576708..f086849 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectImportHelper.kt @@ -151,6 +151,9 @@ object ProjectImportHelper { val memberNameToId = mutableMapOf() val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null)) + // addProject only inserts a subset of the row, currency not among it, so the main + // currency the file declared has to be written separately or it is lost. + if (mainCurrencyName != null) db.updateProject(pid, newCurrencyName = mainCurrencyName) val pmRemoteToLocal = mutableMapOf() paymentModes.forEach { diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt index 9add079..73e928a 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -838,13 +838,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen } /** - * Walks back through pages of bills, newest first, stopping at the first page that already - * matches locally and taking everything older on trust. + * Walks back through pages of bills, newest first, until it has seen a run of + * [UNCHANGED_RUN_TO_SETTLE] consecutive bills that already match locally - at which point + * everything older is taken on trust. * * That only terminates while the server really does reverse the order and honour the * offset. One that does neither returns the same oldest-first page every time, and since - * the walk compares against local rows it never writes, a single unknown bill keeps every - * page mismatched and the same page is requested forever. Returns null when the responses + * the walk compares against local rows it never writes, a single unknown bill keeps the + * run at zero and the same page is requested forever. Returns null when the responses * show that happening, so the caller can fall back to the complete fetch. */ private fun walkBillPages( @@ -857,6 +858,9 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen var syncTimestamp = project.lastSyncedTimestamp var offset = 0 var previousPageIds: List? = null + // Counted across pages, not restarted at each one: a run that begins near the end of + // a page still finishes on the next. + var unchangedRun = 0 while (true) { val response = client!!.getBills(project, offset, limit, true, 0) @@ -881,13 +885,23 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen syncTimestamp = response.syncTimestamp } - val pageAlreadyLocal = page.all { remote -> + var settled = false + for (remote in page) { val local = localBillsByRemoteId[remote.remoteId] - local != null && !hasChanged(local, remote) + if (local != null && !hasChanged(local, remote)) { + unchangedRun++ + if (unchangedRun >= UNCHANGED_RUN_TO_SETTLE) { + settled = true + break + } + } else { + unchangedRun = 0 + } } + // A short page is the end of the collection: there is nothing older to walk back // to, so asking for the next offset would only refetch it. - if (pageAlreadyLocal || page.size < limit) break + if (settled || page.size < limit) break offset += limit } @@ -1803,6 +1817,16 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen companion object { private val TAG = CowspentServerSyncHelper::class.java.simpleName + /** + * How many consecutive bills, walking newest to oldest, must already match locally before + * the paged walk concludes that every older bill matches too. + * + * A whole page of 50 had to match before, so one edit anywhere in a page forced another + * page to be fetched. A trailing run is the same bet on a smaller sample: it can settle + * part way into a page, and it carries across page boundaries rather than restarting. + */ + private const val UNCHANGED_RUN_TO_SETTLE = 25 + private var instance: CowspentServerSyncHelper? = null private val projectIdsToSync: MutableList = ArrayList() diff --git a/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt b/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt index d1feac3..89ae38b 100644 --- a/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt +++ b/app/src/main/java/net/helcel/cowspent/util/ServerResponse.kt @@ -897,7 +897,9 @@ open class ServerResponse( memberRemoteIdToId: Map ): List { val billOwers: MutableList = ArrayList() - if (json.has("owers")) { + // As everywhere else here, an explicitly null value is not a value: getJSONArray would + // throw on it, and that exception fails the whole project sync over one bill. + if (json.has("owers") && !json.isNull("owers")) { val jsonOs = json.getJSONArray("owers") for (i in 0 until jsonOs.length()) { val obj = jsonOs.get(i) diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 9a35300..3afcdf9 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -1,7 +1,6 @@ - Cowspent Neue Rechnung @@ -10,7 +9,11 @@ Bearbeiten Teilen Suchen + Menü öffnen + Suche schließen + Suche leeren Löschen + Zurück Archivieren Reaktivieren Exportieren @@ -37,7 +40,7 @@ Projekt hinzufügen Neue Kategorie Zahlungsmethode hinzufügen - Nextloud-Konto + Nextcloud-Konto Weblink Cowspent link Bist du sicher? @@ -137,10 +140,17 @@ Archivierte Projekte anzeigen Beta-Funktionen Experimentelle Funktionen aktivieren. Benutzung auf eigene Gefahr. + Aus letzter Rechnung vorausfüllen + Zahler, Kategorie, Zahlungsart und Beteiligte aus der zuletzt erstellten Rechnung des Projekts übernehmen. + Synchronisierungsintervall + Wie oft Konto und alle Projekte beim Öffnen der App aktualisiert werden. + 1 Minute + 10 Minuten + 1 Stunde + 1 Tag WARNUNG: \"http\" ist unsicher. Verwende \"https\". Farbe wählen System - Nextcloud Manuell Hell Dunkel @@ -158,7 +168,7 @@ Alle Kreditkarte Bargeld - Prüfen + Scheck Online Überweisung Keine @@ -181,8 +191,6 @@ Was Wo Nur lokal - Cospend - IHateMoney Bestehendem Projekt beitreten Neues Projekt erstellen Aus Datei importieren diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 982ff8f..c7e3ff3 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -1,6 +1,6 @@ - Capucha gastada + Nueva factura Añadir proyecto @@ -8,154 +8,174 @@ Editar Compartir Buscar + Abrir el menú + Cerrar la búsqueda + Borrar la búsqueda Eliminar + Atrás Archivar Desarchivar Exportar Estadísticas - Salir - Scan QR Code + Liquidar + Escanear código QR Ajustes - Categorías faltantes de etiqueta + Categorizar facturas Cerrar sesión Conectar Descartar Miembros Etiquetas Monedas + Estadísticas Editar proyecto - Facturas de etiqueta - Administrar etiquetas + Categorizar facturas + Gestionar etiquetas Acerca de - Liquidar Proyecto + Liquidar proyecto Compartir proyecto Añadir proyecto Añadir categoría Añadir modo de pago - Cuenta Nextcloud + Cuenta de Nextcloud Enlace web - Cowspent link + Enlace de Cowspent ¿Estás seguro? + Todas las facturas Categorías Modos de pago Nombre - Icon / Emoji + Icono / Emoji Color Peso Activado Contraseña - E-mail + Correo electrónico Dirección del servidor - Usuario + Nombre de usuario Comentario ¿Qué? ¿Quién pagó? ¿Para quién? - Repetir cada + Repetición Modo Categoría - ID del proyecto/nombre + ID/nombre del proyecto Título del proyecto - Usar cuenta de la aplicación Nextcloud + Usar la cuenta de la aplicación Nextcloud + Cambios sin guardar - ¿Guardar cambios antes de salir? + ¿Guardar los cambios antes de salir? El proyecto remoto no se eliminará. Error de sincronización - Sincronización fallida para %1$s.\n\n%2$s + Error al sincronizar %1$s.\n\n%2$s Los gastos ya están equilibrados. Proyecto %1$s añadido - Todas las facturas etiquetadas + Todas las facturas están categorizadas No hay sugerencias - Requiere Gasto v0.3.4+. + Requiere Cospend v0.3.4+. Enlace copiado al portapapeles - Escanea el código QR o comparte el enlace para unirte. - Enlace para acceso al navegador web. - Comparte este enlace con un usuario de Cowged. - Acuerdo para %1$s: + Escanea el código QR o comparte el enlace para unirte al proyecto. + Enlace de acceso desde un navegador web. + Comparte este enlace con un usuario de Cowspent. + Liquidación de %1$s: %1$s debe %3$.2f a %2$s Estadísticas de %1$s: - Miembro Pagado | Gasto | Saldo) - Logged in as %1$s + Miembro (Pagado | Gastado | Saldo) + Sesión iniciada como %1$s + Error Cargando No se encontraron proyectos - Ningún miembro encontrado + No se encontraron miembros No se encontraron facturas Se requiere al menos un miembro - El servidor está en modo mantenimiento - 400 Solicitud errónea - 401 no autorizado - 403 Prohibida - 404 no encontrado - Sincronización fallida: %1$s + El servidor está en modo de mantenimiento + 400 Solicitud incorrecta + 401 No autorizado + 403 Prohibido + 404 No encontrado + Error de sincronización: %1$s Inicio de sesión no válido: %1$s Nombre de usuario o contraseña incorrectos - Respuesta del servidor inválida - Petición fallida - E-mail inválido - ID de proyecto inválido - Título del proyecto inválido + Respuesta del servidor no válida + La petición ha fallado + Correo electrónico no válido + ID de proyecto no válido + Título de proyecto no válido Nombre de factura no válido - Fecha de factura inválida + Fecha de factura no válida Pagador requerido - Propietarios requeridos + Participantes requeridos No hay conexión de red Error del servidor - Conexión con el servidor dañada + Se ha perdido la conexión con el servidor No se puede compartir este proyecto + - Conectar a la cuenta de Nextcloud + Conectar a una cuenta de Nextcloud Última sincronización: %1$02d:%2$02d Cancelar Ok - Nu + No Cerrar + Apariencia Red - Otro + Otros Tema Modo sin conexión - Solo sincronizar manualmente. + Sincronizar solo manualmente. Color personalizado Selección de color - Mostrar proyectos archivados - Características beta - Activar características experimentales. Úsalo bajo tu propio riesgo. - ADVERTENCIA: \"http\" no es seguro. Use \"https\". - Elegir color + Mostrar los proyectos archivados + Funciones beta + Activar las funciones experimentales. Úsalas bajo tu propia responsabilidad. + Rellenar desde la última factura + Reutilizar el pagador, la categoría, el modo y los participantes de la última factura creada en el proyecto. + Intervalo de sincronización + Con qué frecuencia se actualizan la cuenta y todos los proyectos al abrir la aplicación. + 1 minuto + 10 minutos + 1 hora + 1 día + ADVERTENCIA: \"http\" no es seguro. Usa \"https\". + Elegir un color + Sistema - Nextcloud Manual Claro Oscuro - Seguir sistema - + Seguir el sistema + - No repetir - Diario + Sin repetición + Diaria Semanal - Fortnocturno + Quincenal Mensual Anual - Ninguna + + Ninguno Todos Tarjeta de crédito - Dinero - Comprobar + Efectivo + Cheque En línea - Transferir + Transferencia + Ninguna - Todos - Todos excepto reembolso - Comestible + Todas + Todas excepto reembolso + Supermercado Bar/Fiesta Alquiler Factura @@ -167,75 +187,81 @@ Alojamiento Transporte Deporte + Qué - Donde + Dónde Solo local - Gastar - Dinero IHate - Unirse al proyecto existente - Crear nuevo proyecto - Importar desde archivo - Elegir proyecto + Unirse a un proyecto existente + Crear un proyecto nuevo + Importar desde un archivo + Elegir un proyecto No se encontraron proyectos en esta cuenta. - Projekt + Proyecto Proyecto añadido correctamente. Aún no tienes proyectos. - Configurar cuenta Nextcloud - Añadir proyecto manualmente + Configurar una cuenta de Nextcloud + Añadir un proyecto manualmente No hay miembros en este proyecto. No se encontraron facturas. El miembro ya existe. Proyecto: %1$s Proyecto %1$s eliminado. Archivo guardado: %1$s - Error al importar en la fila %d + Error de importación en la fila %d Formato de fecha no válido en la fila %d - Dueños no válidos en la fila %d + Participantes no válidos en la fila %d Añadir miembro Editar miembro Eliminar - No hay cambios para guardar. + No hay cambios que guardar. + Ninguno (óptimo) Quién paga - A quien - Cantidad + A quién + Importe Compartir - Crear facturas - Acuerdo + Crear las facturas + Liquidación + - Elija la moneda (%s) + Elegir moneda (%s) Ninguna - Todos + Todas Ajustes de moneda guardados. Moneda principal + Categorías sugeridas - Saltar - De - A + Omitir + Desde + Hasta Miembro Pagado - Escrito + Gastado Saldo Total: %1$s + - Conexión fallida: %1$s - Creación fallida: %1$s - Error al actualizar proyecto remoto: %1$s - Red no disponible para operaciones remotas. - Error al analizar el código QR. - El token de autenticación no coincide. Por favor, inicia sesión de nuevo. + Error de conexión: %1$s + Error al crear: %1$s + Error al actualizar el proyecto remoto: %1$s + Red no disponible para esta operación remota. + No se ha podido leer el código QR. + El token de autenticación no coincide. Vuelve a iniciar sesión. No tienes permiso para realizar esta acción. - Eliminar Etiqueta - ¿Está seguro que desea eliminar esta etiqueta? + Eliminar etiqueta + ¿Seguro que quieres eliminar esta etiqueta? + Versión %1$s Mantenedor Licencia Código fuente + Proyecto %1$s Compartir %1$s + diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 4a8ad6c..a4517b0 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -1,34 +1,40 @@ - Vache dépensée + + Nouvelle facture Ajouter un projet Enregistrer - Editer + Modifier Partager - Chercher - Supprimez - Archive + Rechercher + Ouvrir le menu + Fermer la recherche + Effacer la recherche + Supprimer + Retour + Archiver Désarchiver - Exportation + Exporter Stats Régler - Scan QR Code + Scanner un QR code Réglages - Catégories manquantes - Déconnexion + Catégoriser les factures + Déconnecter Connecter Abandonner Membres Étiquettes Devises + Statistiques Modifier le projet - Étiquettes de factures + Catégoriser les factures Gérer les étiquettes - À propos de + À propos Régler le projet Partager le projet Ajouter un projet @@ -36,14 +42,15 @@ Ajouter un mode de paiement Compte Nextcloud Lien web - Cowspent link + Lien Cowspent Êtes-vous sûr(e) ? + Toutes les factures Catégories Modes de paiement Nom - Icon / Emoji + Icône / Emoji Couleur Poids Activé @@ -51,48 +58,50 @@ Courriel Adresse du serveur Nom d\'utilisateur - Commenter - Quoi? + Commentaire + Quoi ? Qui a payé ? - Pour qui? - Répéter toutes les + Pour qui ? + Répétition Mode Catégorie ID/nom du projet Titre du projet - Utiliser le compte Nextcloud App + Utiliser le compte de l\'application Nextcloud + Modifications non enregistrées - Enregistrer les modifications avant de partir? + Enregistrer les modifications avant de quitter ? Le projet distant ne sera pas supprimé. Erreur de synchronisation Échec de la synchronisation pour %1$s.\n\n%2$s Les dépenses sont déjà équilibrées. Projet %1$s ajouté - Toutes les factures étiquetées + Toutes les factures sont catégorisées Aucune suggestion Nécessite Cospend v0.3.4+. Lien copié dans le presse-papiers - Scannez le code QR ou partagez le lien pour vous inscrire. - Lien pour accéder au navigateur Web. - Partagez ce lien avec un utilisateur Cowspent - Réglage pour %1$s: + Scannez le QR code ou partagez le lien pour rejoindre le projet. + Lien d\'accès depuis un navigateur web. + Partagez ce lien avec un utilisateur de Cowspent. + Règlement pour %1$s : %1$s doit %3$.2f à %2$s - Statistiques pour %1$s: - Membre (payé | Dépensé | Solde) + Statistiques pour %1$s : + Membre (Payé | Dépensé | Solde) Connecté en tant que %1$s + Erreur - En cours de chargement + Chargement Aucun projet trouvé Aucun membre trouvé Aucune facture trouvée Au moins un membre est requis Le serveur est en mode maintenance - 400 Mauvaise requête + 400 Requête incorrecte 401 Non autorisé 403 Interdit - 404 introuvable + 404 Introuvable Échec de la synchronisation : %1$s Identifiant invalide : %1$s Mauvais nom d\'utilisateur ou mot de passe @@ -100,64 +109,78 @@ La requête a échoué Adresse e-mail invalide ID de projet invalide - Titre du projet non valide + Titre de projet invalide Nom de facture invalide Date de facture invalide Payeur requis - Propriétaires requis + Participants requis Aucune connexion réseau Erreur serveur La connexion au serveur a échoué Impossible de partager ce projet + Se connecter au compte Nextcloud Dernière synchronisation : %1$02d:%2$02d - Abandonner + Annuler Ok Oui Non Fermer + Apparence Réseau Autres Thème - Mode hors-ligne + Mode hors ligne Synchroniser uniquement manuellement. Couleur personnalisée Sélection de la couleur Afficher les projets archivés Fonctionnalités bêta - Activer les fonctionnalités expérimentales. Utiliser à vos propres risques. + Activer les fonctionnalités expérimentales. À utiliser à vos risques et périls. + Pré-remplir depuis la dernière facture + Reprendre le payeur, la catégorie, le mode et les participants de la dernière facture créée dans le projet. + Intervalle de synchronisation + Fréquence de rafraîchissement du compte et de tous les projets à l\'ouverture de l\'application. + 1 minute + 10 minutes + 1 heure + 1 jour AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\". Choisir une couleur + Système - Nuage suivant Manuelle - Lumière + Clair Sombre Suivre le système + + Pas de répétition - Tous les jours + Quotidienne Hebdomadaire - Tous les quinze jours - Mensuel - Annuel + Toutes les deux semaines + Mensuelle + Annuelle + Aucun Tous - Carte de crédit + Carte bancaire Espèces - Contrôler + Chèque En ligne - Transférer - Aucun - Tous - Tout sauf remboursement - Épicerie - Barre/Fête - Louer + Virement + + Aucune + Toutes + Toutes sauf remboursement + Courses + Bar/Fête + Loyer Facture Excursion/Culture Santé @@ -167,12 +190,11 @@ Hébergement Transport Sport + - Qu\'est-ce que - Où se trouve - Seulement en local - Codépense - format@@0 IHateMoney + Quoi + + Local uniquement Rejoindre un projet existant Créer un nouveau projet Importer depuis un fichier @@ -180,62 +202,69 @@ Aucun projet trouvé sur ce compte. Projet Le projet a été ajouté avec succès. - Vous n\'avez pas encore de projets. + Vous n\'avez pas encore de projet. Configurer le compte Nextcloud Ajouter un projet manuellement Aucun membre dans ce projet. Aucune facture trouvée. Ce membre existe déjà. - Projet: %1$s + Projet : %1$s Projet %1$s supprimé. Fichier enregistré : %1$s Échec de l\'importation à la ligne %d Format de date invalide à la ligne %d - Propriétés non valides à la ligne %d + Participants invalides à la ligne %d Ajouter un membre Modifier le membre - Supprimez + Supprimer Aucune modification à enregistrer. + - Aucun (optimiste) + Aucun (optimal) Qui paie À qui Montant Partager - Créer des factures - Réglement + Créer les factures + Règlement + Choisir la devise (%s) - Aucun - Tous + Aucune + Toutes Paramètres des devises enregistrés. Devise principale + Catégories suggérées Ignorer - A partir de - À + Du + Au Membre Payé Dépensé Solde Total : %1$s + Échec de la connexion : %1$s Échec de la création : %1$s Erreur lors de la mise à jour du projet distant : %1$s - Réseau indisponible pour le fonctionnement à distance. - Impossible d\'analyser le code QR. - Incompatibilité du jeton d\'authentification. Veuillez vous reconnecter. + Réseau indisponible pour cette opération distante. + Impossible de lire le QR code. + Jeton d\'authentification invalide. Veuillez vous reconnecter. Vous n\'avez pas la permission d\'effectuer cette action. Supprimer l\'étiquette - Êtes-vous sûr de vouloir supprimer ce label ? + Voulez-vous vraiment supprimer cette étiquette ? + Version %1$s Mainteneur Licence Code source + Projet %1$s Partager %1$s + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 873b4fb..aebb910 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ - Cowspent + Cowspent New bill @@ -14,6 +14,7 @@ Close search Clear search Delete + Back Archive Unarchive Export @@ -152,7 +153,7 @@ Choose Color System - Nextcloud + Nextcloud Manual Light Dark @@ -212,8 +213,8 @@ What Where Local only - Cospend - IHateMoney + Cospend + IHateMoney Join existing project Create new project Import from file diff --git a/gradle.properties b/gradle.properties index 4bc4c94..a535b42 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,6 @@ android.nonTransitiveRClass=false org.gradle.warning.mode=all android.uniquePackageNames=false android.dependency.useConstraints=false -android.r8.strictFullModeForKeepRules=false \ No newline at end of file +android.r8.strictFullModeForKeepRules=false +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true