summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/compose
diff options
context:
space:
mode:
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/compose')
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt228
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/LoadingScreen.kt34
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/NumericInputField.kt3
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt21
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/SelectionChip.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/compose/ShareButton.kt26
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)
}
}