commit 5d9d788e4bd1356f5485aae3c988ba16a3e52594
parent 493a7ffeb3d6b6c34643f24a3c9afdb705b920c9
Author: Iván Ávalos <avalos@disroot.org>
Date: Fri, 25 Oct 2024 20:42:57 +0200
[wallet] Add multi-selection to new transaction list
Diffstat:
5 files changed, 241 insertions(+), 32 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -30,7 +30,6 @@ import android.view.View.VISIBLE
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
-import androidx.core.view.GravityCompat.START
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -126,18 +125,6 @@ class MainActivity : AppCompatActivity(), OnPreferenceStartFragmentCallback {
}
}
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- if (ui.drawerLayout.isDrawerOpen(START)) ui.drawerLayout.closeDrawer(START)
- else if (nav.currentDestination?.id == R.id.nav_main) {
- if (model.transactionManager.selectedScope.value != null) {
- model.transactionManager.selectScope(null)
- }
- } else {
- super.onBackPressed()
- }
- }
-
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntents(intent)
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
@@ -20,6 +20,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
@@ -59,6 +61,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -76,7 +79,6 @@ import net.taler.wallet.compose.GridMenuItem
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.settings.SettingsFragment
-import net.taler.wallet.transactions.TransactionsResult
class MainFragment: Fragment() {
@@ -144,10 +146,16 @@ class MainFragment: Fragment() {
}
}
) { innerPadding ->
+ val context = LocalContext.current
val balanceState by model.balanceManager.state.observeAsState(BalanceState.None)
val selectedScope by model.transactionManager.selectedScope.collectAsStateLifecycleAware()
val txResult by remember(selectedScope) { model.transactionManager.transactionsFlow(selectedScope) }.collectAsStateLifecycleAware()
val selectedSpec = remember(selectedScope) { selectedScope?.let { model.balanceManager.getSpecForScopeInfo(it) } }
+
+ BackHandler(selectedScope != null) {
+ model.transactionManager.selectScope(null)
+ }
+
Box(Modifier.padding(innerPadding).fillMaxSize()) {
when (selectedTab) {
Tab.BALANCES -> BalancesComposable(
@@ -164,6 +172,11 @@ class MainFragment: Fragment() {
findNavController().navigate(tx.detailPageNav)
}
},
+ onTransactionsDelete = { txIds ->
+ model.transactionManager.deleteTransactions(txIds) { error ->
+ Toast.makeText(context, error.userFacingMsg, Toast.LENGTH_LONG).show()
+ }
+ },
onShowBalancesClicked = {
if (model.transactionManager.selectedScope.value != null) {
model.transactionManager.selectScope(null)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt
@@ -61,6 +61,7 @@ fun BalancesComposable(
selectedCurrencySpec: CurrencySpecification?,
onBalanceClicked: (balance: BalanceItem) -> Unit,
onTransactionClicked: (tx: Transaction) -> Unit,
+ onTransactionsDelete: (txIds: List<String>) -> Unit,
onShowBalancesClicked: () -> Unit,
) {
when (state) {
@@ -90,6 +91,7 @@ fun BalancesComposable(
currencySpec = selectedCurrencySpec,
txResult = txResult,
onTransactionClick = onTransactionClicked,
+ onTransactionsDelete = onTransactionsDelete,
onShowBalancesClicked = onShowBalancesClicked,
)
} ?: error("no balance matching scopeInfo")
@@ -216,6 +218,7 @@ fun BalancesComposablePreview() {
selectedCurrencySpec = null,
onBalanceClicked = {},
onTransactionClicked = {},
+ onTransactionsDelete = {},
onShowBalancesClicked = {},
)
}
@@ -232,6 +235,7 @@ fun BalancesComposableEmptyPreview() {
selectedCurrencySpec = null,
onBalanceClicked = {},
onTransactionClicked = {},
+ onTransactionsDelete = {},
onShowBalancesClicked = {},
)
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/SelectionModeTopAppBar.kt b/wallet/src/main/java/net/taler/wallet/compose/SelectionModeTopAppBar.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.material.icons.Icons
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.Delete
+import androidx.compose.material.icons.rounded.SelectAll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.res.stringResource
+import net.taler.wallet.R
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun SelectionModeTopAppBar(
+ selectedItems: SnapshotStateList<String>,
+ resetSelectionMode: () -> Unit,
+ onSelectAllClicked: () -> Unit,
+ onDeleteClicked: () -> Unit,
+) {
+ TopAppBar(
+ title = {
+ Text(
+ text = "${selectedItems.size} selected",
+ style = MaterialTheme.typography.titleLarge.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ ),
+ )
+ },
+
+ navigationIcon = {
+ IconButton(resetSelectionMode) {
+ Icon(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ },
+
+ actions = {
+ IconButton(onDeleteClicked) {
+ Icon(
+ imageVector = Icons.Rounded.Delete,
+ contentDescription = stringResource(R.string.transactions_delete),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+
+ IconButton(onSelectAllClicked) {
+ Icon(
+ imageVector = Icons.Rounded.SelectAll,
+ contentDescription = stringResource(R.string.transactions_select_all),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ },
+
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ ),
+ )
+}
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt
@@ -16,27 +16,42 @@
package net.taler.wallet.transactions
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.material.icons.rounded.RadioButtonUnchecked
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Badge
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -58,6 +73,7 @@ import net.taler.wallet.balances.BalanceItem
import net.taler.wallet.balances.ScopeInfo.Exchange
import net.taler.wallet.cleanExchange
import net.taler.wallet.compose.LoadingScreen
+import net.taler.wallet.compose.SelectionModeTopAppBar
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.transactions.AmountType.Negative
import net.taler.wallet.transactions.AmountType.Neutral
@@ -84,12 +100,66 @@ fun TransactionsComposable(
currencySpec: CurrencySpecification?,
txResult: TransactionsResult,
onTransactionClick: (tx: Transaction) -> Unit,
+ onTransactionsDelete: (txIds: List<String>) -> Unit,
onShowBalancesClicked: () -> Unit,
-) {
- when (txResult) {
- is None -> LoadingScreen()
- is Error -> {} // TODO: render error!
- is Success -> {
+) = when (txResult) {
+ is None -> LoadingScreen()
+ is Error -> {} // TODO: render error!
+ is Success -> {
+ var showDeleteDialog by remember { mutableStateOf(false) }
+ var selectionMode by remember { mutableStateOf(false) }
+ val selectedItems = remember { mutableStateListOf<String>() }
+
+ if (showDeleteDialog) AlertDialog(
+ title = { Text(stringResource(R.string.transactions_delete_dialog_title)) },
+ text = { Text(stringResource(R.string.transactions_delete_dialog_message)) },
+ onDismissRequest = { showDeleteDialog = false },
+ confirmButton = {
+ TextButton(onClick = {
+ onTransactionsDelete(selectedItems)
+ selectedItems.clear()
+ selectionMode = false
+ showDeleteDialog = false
+ }) {
+ Text(stringResource(R.string.transactions_delete))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = {
+ showDeleteDialog = false
+ }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ )
+
+ BackHandler(selectionMode) {
+ selectionMode = false
+ selectedItems.clear()
+ }
+
+ LaunchedEffect(selectionMode, selectedItems.size) {
+ if (selectionMode && selectedItems.isEmpty()) {
+ selectionMode = false
+ }
+ }
+
+ Column(Modifier.fillMaxSize()) {
+ if (selectionMode) SelectionModeTopAppBar(
+ selectedItems = selectedItems,
+ resetSelectionMode = {
+ selectionMode = false
+ selectedItems.clear()
+ },
+ onSelectAllClicked = {
+ selectedItems.clear()
+ selectedItems += txResult.transactions.map { it.transactionId }
+ },
+ onDeleteClicked = {
+ showDeleteDialog = true
+ },
+ )
+
LazyColumn(Modifier.fillMaxHeight()) {
item {
TransactionsHeader(
@@ -100,9 +170,36 @@ fun TransactionsComposable(
}
items(txResult.transactions, key = { it.transactionId }) { tx ->
- TransactionRow(tx, currencySpec) {
- onTransactionClick(tx)
- }
+ val isSelected = selectedItems.contains(tx.transactionId)
+
+ TransactionRow(
+ tx, currencySpec,
+ isSelected = isSelected,
+ selectionMode = selectionMode,
+ onTransactionClick = {
+ if (selectionMode) {
+ if (isSelected) {
+ selectedItems.remove(tx.transactionId)
+ } else {
+ selectedItems.add(tx.transactionId)
+ }
+ } else {
+ onTransactionClick(tx)
+ }
+ },
+ onTransactionSelect = {
+ if (selectionMode) {
+ if (isSelected) {
+ selectedItems.remove(tx.transactionId)
+ } else {
+ selectedItems.add(tx.transactionId)
+ }
+ } else {
+ selectionMode = true
+ selectedItems.add(tx.transactionId)
+ }
+ },
+ )
}
}
}
@@ -127,14 +224,12 @@ fun TransactionsHeader(
) {
ListItem(
modifier = Modifier.animateContentSize(),
-
headlineContent = {
Text(
getHeaderCurrency(balance, spec),
style = MaterialTheme.typography.titleMedium,
)
},
-
supportingContent = {
if (balance.scopeInfo is Exchange) {
Text(
@@ -144,7 +239,6 @@ fun TransactionsHeader(
)
}
},
-
trailingContent = {
Icon(Icons.Default.ArrowDropDown, contentDescription = null)
}
@@ -170,19 +264,25 @@ fun TransactionsHeader(
}
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TransactionRow(
tx: Transaction,
spec: CurrencySpecification?,
+ isSelected: Boolean,
+ selectionMode: Boolean,
onTransactionClick: () -> Unit,
+ onTransactionSelect: () -> Unit,
) {
val context = LocalContext.current
ListItem(
modifier = Modifier
.defaultMinSize(minHeight = 80.dp)
- .clickable { onTransactionClick() },
-
+ .combinedClickable(
+ onClick = onTransactionClick,
+ onLongClick = onTransactionSelect,
+ ),
trailingContent = {
Box(
modifier = Modifier.padding(8.dp),
@@ -191,16 +291,28 @@ fun TransactionRow(
TransactionAmountInfo(tx, spec)
}
},
-
leadingContent = {
Box(
modifier = Modifier.padding(8.dp),
contentAlignment = Alignment.Center,
) {
- Icon(painterResource(tx.icon), contentDescription = null)
+ if (!selectionMode) {
+ Icon(painterResource(tx.icon), contentDescription = null)
+ } else if (isSelected) {
+ Icon(
+ Icons.Rounded.CheckCircle,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ } else {
+ Icon(
+ Icons.Rounded.RadioButtonUnchecked,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.outline,
+ )
+ }
}
},
-
headlineContent = {
Text(
tx.getTitle(context),
@@ -208,12 +320,17 @@ fun TransactionRow(
style = MaterialTheme.typography.titleMedium,
)
},
-
supportingContent = {
TransactionExtraInfo(tx)
},
-
overlineContent = { Text(tx.timestamp.ms.toRelativeTime(context).toString()) },
+ colors = ListItemDefaults.colors(
+ containerColor = if (isSelected) {
+ MaterialTheme.colorScheme.secondaryContainer
+ } else {
+ ListItemDefaults.containerColor
+ }
+ )
)
}
@@ -326,6 +443,7 @@ fun TransactionsComposableDonePreview() {
currencySpec = null,
txResult = Success(transactions),
onTransactionClick = {},
+ onTransactionsDelete = {},
onShowBalancesClicked = {},
)
}
@@ -354,6 +472,7 @@ fun TransactionsComposablePendingPreview() {
currencySpec = null,
txResult = Success(transactions),
onTransactionClick = {},
+ onTransactionsDelete = {},
onShowBalancesClicked = {},
)
}