diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/item/ItemScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/item/ItemScreen.kt index 4da5aaafa5..c50fce3c15 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/item/ItemScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/item/ItemScreen.kt @@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FabPosition -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarDefaults @@ -81,7 +81,7 @@ fun ItemScreen( enter = scaleIn(), exit = scaleOut(), ) { - FloatingActionButton( + ExtendedFloatingActionButton( containerColor = MaterialTheme.colorScheme.primaryContainer, onClick = remember(viewModel) { { onNavigateToEditItem(state.itemId) } }, modifier = Modifier diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt index 03d1383d27..c9d7309eff 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt @@ -21,9 +21,10 @@ fun NavGraphBuilder.itemListingGraph( ) { itemListingDestination( onNavigateBack = { navController.popBackStack() }, - onNavigateToAddItemScreen = { /*navController.navigateToAddItem()*/ }, + onNavigateToQrCodeScanner = { /*navController.navigateToQrCodeScanner()*/ }, onNavigateToItemScreen = { navController.navigateToItem(itemId = it) }, onNavigateToEditItemScreen = { /*navController.navigateToEditItem(itemId = it)*/ }, + onNavigateToManualKeyEntry = { /*navController.navigateToManualKeySetup()*/ }, ) itemDestination( onNavigateBack = { navController.popBackStack() }, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt index f50be3b0cc..eb29eb6663 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingNavigation.kt @@ -10,7 +10,8 @@ const val ITEM_LIST_ROUTE = "item_list" */ fun NavGraphBuilder.itemListingDestination( onNavigateBack: () -> Unit = { }, - onNavigateToAddItemScreen: () -> Unit = { }, + onNavigateToQrCodeScanner: () -> Unit = { }, + onNavigateToManualKeyEntry: () -> Unit = { }, onNavigateToItemScreen: (id: String) -> Unit = { }, onNavigateToEditItemScreen: (id: String) -> Unit = { }, ) { @@ -19,7 +20,8 @@ fun NavGraphBuilder.itemListingDestination( ) { ItemListingScreen( onNavigateBack = onNavigateBack, - onNavigateToAddItemScreen = onNavigateToAddItemScreen, + onNavigateToQrCodeScanner = onNavigateToQrCodeScanner, + onNavigateToManualKeyEntry = onNavigateToManualKeyEntry, onNavigateToItemScreen = onNavigateToItemScreen, onNavigateToEditItemScreen = onNavigateToEditItemScreen ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index d71eee7b6d..0318b1894f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -9,16 +9,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -30,6 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.authenticator.R +import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar @@ -37,6 +34,9 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BasicDial import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState +import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon +import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton +import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold /** @@ -47,7 +47,8 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.Bitward fun ItemListingScreen( viewModel: ItemListingViewModel = hiltViewModel(), onNavigateBack: () -> Unit, - onNavigateToAddItemScreen: () -> Unit, + onNavigateToQrCodeScanner: () -> Unit, + onNavigateToManualKeyEntry: () -> Unit, onNavigateToItemScreen: (id: String) -> Unit, onNavigateToEditItemScreen: (id: String) -> Unit, ) { @@ -60,7 +61,8 @@ fun ItemListingScreen( when (event) { ItemListingEvent.NavigateBack -> onNavigateBack() ItemListingEvent.DismissPullToRefresh -> pullToRefreshState.endRefresh() - ItemListingEvent.NavigateToAddItem -> onNavigateToAddItemScreen() + ItemListingEvent.NavigateToQrCodeScanner -> onNavigateToQrCodeScanner() + ItemListingEvent.NavigateToManualAddItem -> onNavigateToManualKeyEntry() is ItemListingEvent.NavigateToItem -> onNavigateToItemScreen(event.id) is ItemListingEvent.ShowToast -> { Toast.makeText(context, event.message(context.resources), Toast.LENGTH_LONG) @@ -83,20 +85,48 @@ fun ItemListingScreen( ) }, floatingActionButton = { - FloatingActionButton( - containerColor = MaterialTheme.colorScheme.primaryContainer, - onClick = remember(viewModel) { - { viewModel.trySendAction(ItemListingAction.AddItemClick) } - }, + ExpandableFloatingActionButton( modifier = Modifier .semantics { testTag = "AddItemButton" } .padding(bottom = 16.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_plus), - contentDescription = stringResource(id = R.string.add_item), - ) - } + label = R.string.add_item.asText(), + items = listOf( + ItemListingExpandableFabAction.ScanQrCode( + label = R.string.scan_a_qr_code.asText(), + icon = IconResource( + iconPainter = painterResource(id = R.drawable.ic_camera), + contentDescription = stringResource(id = R.string.scan_a_qr_code), + testTag = "ScanQRCodeButton" + ), + onScanQrCodeClick = { + viewModel.trySendAction( + ItemListingAction.ScanQrCodeClick + ) + } + ), + ItemListingExpandableFabAction.EnterSetupKey( + label = R.string.enter_a_setup_key.asText(), + icon = IconResource( + iconPainter = painterResource(id = R.drawable.ic_copy), + contentDescription = stringResource(id = R.string.enter_a_setup_key), + testTag = "EnterSetupKeyButton" + ), + onEnterSetupKeyClick = { + viewModel.trySendAction( + ItemListingAction.EnterSetupKeyClick + ) + } + ) + ), + expandableFabIcon = ExpandableFabIcon( + iconData = IconResource( + iconPainter = painterResource(id = R.drawable.ic_plus), + contentDescription = stringResource(id = R.string.add_item), + testTag = "AddItemButton", + ), + iconRotation = 45f, + ), + ) }, floatingActionButtonPosition = FabPosition.EndOverlay, ) { paddingValues -> diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt index 08438f1336..635438908f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingViewModel.kt @@ -91,9 +91,9 @@ class ItemListingViewModel @Inject constructor( override fun handleAction(action: ItemListingAction) { when (action) { - ItemListingAction.AddItemClick -> sendEvent(ItemListingEvent.NavigateToAddItem) + ItemListingAction.ScanQrCodeClick -> sendEvent(ItemListingEvent.NavigateToQrCodeScanner) + ItemListingAction.EnterSetupKeyClick -> sendEvent(ItemListingEvent.NavigateToManualAddItem) ItemListingAction.BackClick -> sendEvent(ItemListingEvent.NavigateBack) - is ItemListingAction.ItemClick -> sendEvent(ItemListingEvent.NavigateToItem(action.id)) ItemListingAction.DialogDismiss -> handleDialogDismiss() is ItemListingAction.Internal.AuthenticatorDataReceive -> handleAuthenticatorDataReceive( @@ -276,9 +276,14 @@ sealed class ItemListingEvent { data object NavigateBack : ItemListingEvent() /** - * Navigate to the Add Item screen. + * Navigate to the QR Code Scanner screen. */ - data object NavigateToAddItem : ItemListingEvent() + data object NavigateToQrCodeScanner : ItemListingEvent() + + /** + * Navigate to the Manual Add Item screen. + */ + data object NavigateToManualAddItem : ItemListingEvent() /** * Navigate to the View Item screen. @@ -314,9 +319,14 @@ sealed class ItemListingAction { data object BackClick : ItemListingAction() /** - * The user clicked the Add Item button. + * The user clicked the Scan QR Code button. */ - data object AddItemClick : ItemListingAction() + data object ScanQrCodeClick : ItemListingAction() + + /** + * The user clicked the Enter Setup Key button. + */ + data object EnterSetupKeyClick : ItemListingAction() /** * The user clicked a list item. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt new file mode 100644 index 0000000000..6f58764d50 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model + +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text +import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabOption +import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource + +sealed class ItemListingExpandableFabAction( + label: Text?, + icon: IconResource, + onFabOptionClick: () -> Unit, +) : ExpandableFabOption(label, icon, onFabOptionClick) { + + class ScanQrCode( + label: Text?, + icon: IconResource, + onScanQrCodeClick: () -> Unit, + ) : ItemListingExpandableFabAction( + label, + icon, + onScanQrCodeClick + ) + + class EnterSetupKey( + label: Text?, + icon: IconResource, + onEnterSetupKeyClick: () -> Unit, + ) : ItemListingExpandableFabAction( + label, + icon, + onEnterSetupKeyClick + ) + +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/fab/ExpandableFloatingActionButton.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/fab/ExpandableFloatingActionButton.kt new file mode 100644 index 0000000000..ec2c0bf237 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/fab/ExpandableFloatingActionButton.kt @@ -0,0 +1,180 @@ +package com.x8bit.bitwarden.authenticator.ui.platform.components.fab + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.authenticator.R +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text +import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource +import com.x8bit.bitwarden.authenticator.ui.platform.theme.Typography + +@Composable +fun ExpandableFloatingActionButton( + modifier: Modifier = Modifier, + label: Text?, + items: List, + expandableFabState: MutableState = rememberExpandableFabState(), + expandableFabIcon: ExpandableFabIcon, + onStateChange: (expandableFabState: ExpandableFabState) -> Unit = { }, +) { + val rotation by animateFloatAsState( + targetValue = if (expandableFabState.value == ExpandableFabState.Expanded) { + expandableFabIcon.iconRotation ?: 0f + } else { + 0f + }, + label = stringResource(R.string.add_item_rotation), + ) + Column( + modifier = modifier.wrapContentSize(), + horizontalAlignment = Alignment.End, + ) { + AnimatedVisibility( + visible = expandableFabState.value.isExpanded(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + LazyColumn( + modifier = Modifier + .wrapContentSize() + .padding(bottom = 16.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(items) { expandableFabOption -> + ExpandableFabOption( + onFabOptionClick = { + expandableFabState.value = expandableFabState.value.toggleValue() + onStateChange(expandableFabState.value) + expandableFabOption.onFabOptionClick() + }, + expandableFabOption = expandableFabOption + ) + } + } + } + + ExtendedFloatingActionButton( + onClick = { + expandableFabState.value = expandableFabState.value.toggleValue() + onStateChange(expandableFabState.value) + }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + ) { + + if (label != null) { + AnimatedVisibility( + visible = expandableFabState.value.isExpanded(), + enter = fadeIn() + expandHorizontally(), + exit = fadeOut() + shrinkHorizontally(), + ) { + Text( + modifier = Modifier.padding(end = 8.dp), + text = label(), + ) + } + } + + Icon( + modifier = Modifier + .rotate(rotation) + .semantics { expandableFabIcon.iconData.testTag }, + painter = expandableFabIcon.iconData.iconPainter, + contentDescription = expandableFabIcon.iconData.contentDescription, + ) + } + } +} + +@Composable +private fun ExpandableFabOption( + expandableFabOption: T, + onFabOptionClick: (option: T) -> Unit, +) { + SmallFloatingActionButton( + onClick = { onFabOptionClick(expandableFabOption) }, + ) { + Row( + modifier = Modifier + .wrapContentSize() + .padding(end = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + expandableFabOption.label?.let { label -> + Text( + modifier = Modifier + .clip(RoundedCornerShape(size = 8.dp)) + .padding(all = 8.dp), + text = label(), + style = Typography.labelSmall, + ) + } + + Icon( + painter = expandableFabOption.iconData.iconPainter, + contentDescription = expandableFabOption.iconData.contentDescription, + ) + } + } +} + +@Composable +fun rememberExpandableFabState() = + remember { mutableStateOf(ExpandableFabState.Collapsed) } + +abstract class ExpandableFabOption( + val label: Text?, + val iconData: IconResource, + val onFabOptionClick: () -> Unit, +) + +data class ExpandableFabIcon( + val iconData: IconResource, + val iconRotation: Float?, +) + +sealed class ExpandableFabState { + + fun isExpanded() = this is Expanded + + fun toggleValue() = if (isExpanded()) { + Collapsed + } else { + Expanded + } + + data object Collapsed : ExpandableFabState() + + data object Expanded : ExpandableFabState() +} diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000000..c7af235783 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52279b7e6a..d273b5f116 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,4 +18,7 @@ View Item Bitwarden Authenticator Name + Add Item Rotation + Scan a QR code + Enter a setup key