diff options
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/compose')
6 files changed, 284 insertions, 30 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt new file mode 100644 index 0000000..d2a877b --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt @@ -0,0 +1,228 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.compose + +import android.os.Build +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.OutlinedTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import net.taler.common.Amount +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.roundToLong + +const val DEFAULT_INPUT_DECIMALS = 2 + +@Composable +fun AmountInputField( + value: String, + onValueChange: (value: String) -> Unit, + modifier: Modifier = Modifier, + label: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + keyboardActions: KeyboardActions = KeyboardActions.Default, + decimalFormatSymbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols, + numberOfDecimals: Int = DEFAULT_INPUT_DECIMALS, + readOnly: Boolean = false, +) { + var amountInput by remember { mutableStateOf(value) } + + // React to external changes + val amountValue = remember(amountInput, value) { + transformOutput(amountInput).let { + if (value != it) transformInput(value, numberOfDecimals) else amountInput + } + } + + OutlinedTextField( + value = amountValue, + onValueChange = { input -> + if (input.matches("0+".toRegex())) { + amountInput = "0" + onValueChange("") + } else transformOutput(input, numberOfDecimals)?.let { filtered -> + if (Amount.isValidAmountStr(filtered) && !input.contains("-")) { + amountInput = input.trimStart('0') + onValueChange(filtered) + } + } + }, + modifier = modifier, + readOnly = readOnly, + textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace), + label = label, + supportingText = supportingText, + isError = isError, + visualTransformation = AmountInputVisualTransformation( + symbols = decimalFormatSymbols, + fixedCursorAtTheEnd = true, + numberOfDecimals = numberOfDecimals, + ), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword), + keyboardActions = keyboardActions, + singleLine = true, + maxLines = 1, + ) +} + +// 500 -> 5.0 +private fun transformOutput( + input: String, + numberOfDecimals: Int = 2, +) = if (input.isEmpty()) "0" else { + input.toLongOrNull()?.let { it / 10.0.pow(numberOfDecimals) }?.toBigDecimal()?.toPlainString() +} + +// 5.0 -> 500 +private fun transformInput( + output: String, + numberOfDecimals: Int = 2, +) = if (output.isEmpty()) "0" else { + (output.toDouble() * 10.0.pow(numberOfDecimals)).roundToLong().toString() +} + +// Source: https://github.com/banmarkovic/CurrencyAmountInput + +private class AmountInputVisualTransformation( + private val symbols: DecimalFormatSymbols, + private val fixedCursorAtTheEnd: Boolean = true, + private val numberOfDecimals: Int = 2, +): VisualTransformation { + + override fun filter(text: AnnotatedString): TransformedText { + val thousandsSeparator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + symbols.monetaryGroupingSeparator + } else { + symbols.groupingSeparator + } + val decimalSeparator = symbols.monetaryDecimalSeparator + val zero = symbols.zeroDigit + + val inputText = text.text + + val intPart = inputText + .dropLast(numberOfDecimals) + .reversed() + .chunked(3) + .joinToString(thousandsSeparator.toString()) + .reversed() + .ifEmpty { + zero.toString() + } + + val fractionPart = inputText.takeLast(numberOfDecimals).let { + if (it.length != numberOfDecimals) { + List(numberOfDecimals - it.length) { + zero + }.joinToString("") + it + } else { + it + } + } + + // Hide trailing decimal separator if decimals are 0 + val formattedNumber = if (numberOfDecimals > 0) { + intPart + decimalSeparator + fractionPart + } else { + intPart + } + + val newText = AnnotatedString( + text = formattedNumber, + spanStyles = text.spanStyles, + paragraphStyles = text.paragraphStyles + ) + + val offsetMapping = if (fixedCursorAtTheEnd) { + FixedCursorOffsetMapping( + contentLength = inputText.length, + formattedContentLength = formattedNumber.length + ) + } else { + MovableCursorOffsetMapping( + unmaskedText = text.toString(), + maskedText = newText.toString(), + decimalDigits = numberOfDecimals + ) + } + + return TransformedText(newText, offsetMapping) + } + + private class FixedCursorOffsetMapping( + private val contentLength: Int, + private val formattedContentLength: Int, + ) : OffsetMapping { + override fun originalToTransformed(offset: Int): Int = formattedContentLength + override fun transformedToOriginal(offset: Int): Int = contentLength + } + + private class MovableCursorOffsetMapping( + private val unmaskedText: String, + private val maskedText: String, + private val decimalDigits: Int + ) : OffsetMapping { + override fun originalToTransformed(offset: Int): Int = + when { + unmaskedText.length <= decimalDigits -> { + maskedText.length - (unmaskedText.length - offset) + } + else -> { + offset + offsetMaskCount(offset, maskedText) + } + } + + override fun transformedToOriginal(offset: Int): Int = + when { + unmaskedText.length <= decimalDigits -> { + max(unmaskedText.length - (maskedText.length - offset), 0) + } + else -> { + offset - maskedText.take(offset).count { !it.isDigit() } + } + } + + private fun offsetMaskCount(offset: Int, maskedText: String): Int { + var maskOffsetCount = 0 + var dataCount = 0 + for (maskChar in maskedText) { + if (!maskChar.isDigit()) { + maskOffsetCount++ + } else if (++dataCount > offset) { + break + } + } + return maskOffsetCount + } + } +}
\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt b/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt new file mode 100644 index 0000000..6412d63 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt @@ -0,0 +1,34 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun LoadingScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } +}
\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt index c9d2fc5..47401cf 100644 --- a/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt +++ b/wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt @@ -20,14 +20,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -@OptIn(ExperimentalMaterial3Api::class) @Composable fun NumericInputField( modifier: Modifier = Modifier, @@ -41,6 +39,7 @@ fun NumericInputField( OutlinedTextField( modifier = modifier, value = value.toString(), + singleLine = true, readOnly = readOnly, onValueChange = { val dd = it.toLongOrNull() ?: 0 diff --git a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt index 2d7ffa1..4991094 100644 --- a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt @@ -25,21 +25,21 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.runtime.Composable import androidx.compose.runtime.produceState -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap @@ -136,14 +136,13 @@ fun CopyToClipboardButton( colors = colors, onClick = { copyToClipBoard(context, label, content) }, ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.ContentCopy, buttonText) - Text( - modifier = Modifier.padding(start = 8.dp), - text = buttonText, - style = MaterialTheme.typography.bodyLarge, - ) - } + Icon( + Icons.Default.ContentCopy, + buttonText, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(buttonText) } } diff --git a/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt b/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt index 454bbfa..c47f55d 100644 --- a/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt +++ b/wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt @@ -16,7 +16,6 @@ package net.taler.wallet.compose -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChipDefaults @@ -24,7 +23,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -@OptIn(ExperimentalMaterial3Api::class) @Composable fun <T> SelectionChip( label: @Composable () -> Unit, diff --git a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt index ebf2a2f..f3a84dd 100644 --- a/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt +++ b/wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt @@ -19,22 +19,19 @@ package net.taler.wallet.compose import android.content.Intent import android.content.Intent.ACTION_SEND import android.content.Intent.EXTRA_TEXT -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity import net.taler.wallet.R @@ -59,13 +56,12 @@ fun ShareButton( startActivity(context, shareIntent, null) }, ) { - Row(verticalAlignment = CenterVertically) { - Icon(Icons.Default.Share, buttonText) - Text( - modifier = Modifier.padding(start = 8.dp), - text = buttonText, - style = MaterialTheme.typography.bodyLarge, - ) - } + Icon( + Icons.Default.Share, + buttonText, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(buttonText) } } |