TalerActionButton.kt (9425B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.main 18 19 import androidx.compose.animation.core.Animatable 20 import androidx.compose.foundation.gestures.Orientation 21 import androidx.compose.foundation.gestures.draggable 22 import androidx.compose.foundation.gestures.rememberDraggableState 23 import androidx.compose.foundation.layout.Box 24 import androidx.compose.foundation.layout.Column 25 import androidx.compose.foundation.layout.PaddingValues 26 import androidx.compose.foundation.layout.WindowInsets 27 import androidx.compose.foundation.layout.asPaddingValues 28 import androidx.compose.foundation.layout.offset 29 import androidx.compose.foundation.layout.padding 30 import androidx.compose.foundation.layout.requiredSize 31 import androidx.compose.foundation.layout.size 32 import androidx.compose.foundation.layout.systemBars 33 import androidx.compose.foundation.shape.CircleShape 34 import androidx.compose.material.icons.Icons 35 import androidx.compose.material.icons.filled.LocationOn 36 import androidx.compose.material3.ExperimentalMaterial3Api 37 import androidx.compose.material3.Icon 38 import androidx.compose.material3.LargeFloatingActionButton 39 import androidx.compose.material3.ModalBottomSheet 40 import androidx.compose.material3.PlainTooltip 41 import androidx.compose.material3.SheetState 42 import androidx.compose.material3.Text 43 import androidx.compose.material3.TooltipBox 44 import androidx.compose.material3.TooltipDefaults 45 import androidx.compose.material3.rememberModalBottomSheetState 46 import androidx.compose.material3.rememberTooltipState 47 import androidx.compose.runtime.Composable 48 import androidx.compose.runtime.LaunchedEffect 49 import androidx.compose.runtime.getValue 50 import androidx.compose.runtime.mutableStateOf 51 import androidx.compose.runtime.remember 52 import androidx.compose.runtime.setValue 53 import androidx.compose.ui.Modifier 54 import androidx.compose.ui.res.painterResource 55 import androidx.compose.ui.res.stringResource 56 import androidx.compose.ui.tooling.preview.Preview 57 import androidx.compose.ui.unit.IntOffset 58 import androidx.compose.ui.unit.dp 59 import kotlinx.coroutines.runBlocking 60 import net.taler.wallet.R 61 import net.taler.wallet.compose.DemandAttention 62 import net.taler.wallet.compose.GridMenu 63 import net.taler.wallet.compose.GridMenuItem 64 import net.taler.wallet.compose.Material3MenuGroup 65 import net.taler.wallet.compose.Material3MenuItemData 66 import net.taler.wallet.compose.TalerSurface 67 import kotlin.math.roundToInt 68 69 @Composable 70 @OptIn(ExperimentalMaterial3Api::class) 71 fun TalerActionButton( 72 demandAttention: Boolean, 73 onShowSheet: () -> Unit, 74 onScanQr: () -> Unit, 75 ) { 76 val tooltipState = rememberTooltipState() 77 TooltipBox( 78 positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), 79 tooltip = { PlainTooltip { Text(stringResource(R.string.actions)) } }, 80 state = tooltipState, 81 ) { 82 val offsetY = remember { Animatable(0f) } 83 var cancelled by remember { mutableStateOf(false) } 84 85 DemandAttention(demandAttention = demandAttention) { 86 LargeFloatingActionButton( 87 modifier = Modifier 88 .requiredSize(86.dp) 89 .padding(8.dp) 90 .offset { IntOffset(0, offsetY.value.roundToInt() / 6) } 91 .draggable( 92 orientation = Orientation.Vertical, 93 state = rememberDraggableState { delta -> 94 runBlocking { offsetY.snapTo(offsetY.value + delta) } 95 if (delta > 0) { 96 cancelled = true 97 } 98 }, 99 onDragStopped = { 100 offsetY.animateTo(0.0f) 101 if (!cancelled) { 102 onScanQr() 103 } 104 cancelled = false 105 }, 106 ), 107 shape = CircleShape, 108 onClick = { onShowSheet() }, 109 ) { 110 if (offsetY.value == 0.0f) { 111 Icon( 112 painterResource(R.drawable.ic_actions), 113 modifier = Modifier.size(38.dp), 114 contentDescription = stringResource(R.string.actions), 115 ) 116 } else { 117 Icon( 118 painterResource(R.drawable.ic_scan_qr), 119 contentDescription = stringResource(R.string.actions), 120 ) 121 } 122 } 123 } 124 } 125 126 LaunchedEffect(demandAttention) { 127 if (demandAttention) { 128 tooltipState.show() 129 } 130 } 131 } 132 133 @OptIn(ExperimentalMaterial3Api::class) 134 @Composable 135 fun TalerActionsModal( 136 showSheet: Boolean, 137 sheetState: SheetState, 138 selectedCurrency: String? = null, 139 showShopping: Boolean, 140 disableActions: Boolean, 141 disablePeer: Boolean, 142 onDismiss: () -> Unit, 143 onSend: () -> Unit, 144 onReceive: () -> Unit, 145 onScanQr: () -> Unit, 146 onDeposit: () -> Unit, 147 onWithdraw: () -> Unit, 148 onEnterUri: () -> Unit, 149 onShoppingDiscovery: () -> Unit, 150 ) { 151 if (showSheet) ModalBottomSheet( 152 onDismissRequest = onDismiss, 153 sheetState = sheetState, 154 ) { 155 Column { 156 if (showShopping && selectedCurrency != null) { 157 Box(Modifier 158 .padding(horizontal = 12.dp) 159 .padding(bottom = 9.dp) 160 ) { 161 Material3MenuGroup(items = buildList { 162 add( 163 Material3MenuItemData( 164 title = { Text(stringResource(R.string.exchange_shopping_label, selectedCurrency)) }, 165 icon = { Icon( 166 Icons.Default.LocationOn, 167 contentDescription = null 168 ) }, 169 onClick = onShoppingDiscovery, 170 ) 171 ) 172 }) 173 } 174 } 175 176 GridMenu( 177 contentPadding = PaddingValues( 178 start = 8.dp, 179 end = 8.dp, 180 bottom = 8.dp + WindowInsets 181 .systemBars 182 .asPaddingValues() 183 .calculateBottomPadding(), 184 ), 185 ) { 186 GridMenuItem( 187 icon = R.drawable.ic_link, 188 title = R.string.enter_uri, 189 onClick = { onEnterUri(); onDismiss() }, 190 ) 191 192 GridMenuItem( 193 icon = R.drawable.transaction_deposit, 194 title = R.string.send_deposit_button_label, 195 onClick = { onDeposit(); onDismiss() }, 196 enabled = !disableActions 197 ) 198 199 GridMenuItem( 200 icon = R.drawable.ic_scan_qr, 201 title = R.string.button_scan_qr_code_label, 202 onClick = { onScanQr(); onDismiss() }, 203 ) 204 205 GridMenuItem( 206 icon = R.drawable.transaction_p2p_incoming, 207 title = R.string.transactions_receive_funds, 208 onClick = { onReceive(); onDismiss() }, 209 enabled = !disableActions && !disablePeer, 210 ) 211 212 GridMenuItem( 213 icon = R.drawable.transaction_withdrawal, 214 title = R.string.withdraw_button_label, 215 onClick = { onWithdraw(); onDismiss() }, 216 enabled = !disableActions, 217 ) 218 219 GridMenuItem( 220 icon = R.drawable.transaction_p2p_outgoing, 221 title = R.string.transactions_send_funds, 222 onClick = { onSend(); onDismiss() }, 223 enabled = !disableActions && !disablePeer, 224 ) 225 } 226 } 227 } 228 } 229 230 @OptIn(ExperimentalMaterial3Api::class) 231 @Preview 232 @Composable 233 fun TalerActionsModalPreview() { 234 TalerSurface { 235 TalerActionsModal( 236 showSheet = true, 237 sheetState = rememberModalBottomSheetState(), 238 selectedCurrency = "CHF", 239 showShopping = true, 240 disableActions = false, 241 disablePeer = false, 242 onDismiss = {}, 243 onSend = {}, 244 onReceive = {}, 245 onScanQr = {}, 246 onDeposit = {}, 247 onWithdraw = {}, 248 onEnterUri = {}, 249 onShoppingDiscovery = {}, 250 ) 251 } 252 }