taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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 }