From 2bd0df2910bb6a6d01b1c27b1f0d73ba8edc8117 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:02:18 -0400 Subject: [PATCH] Adjust top app bar (#58) --- .../feature/itemlisting/ItemListingScreen.kt | 428 +++++++++++------- .../action/BitwardenSearchActionItem.kt | 45 ++ .../components/util/RememberVectorPainter.kt | 19 + 3 files changed, 327 insertions(+), 165 deletions(-) create mode 100644 app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/appbar/action/BitwardenSearchActionItem.kt create mode 100644 app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/util/RememberVectorPainter.kt diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index c4774f955e..f820dd4ca9 100644 --- a/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -6,7 +6,6 @@ import android.net.Uri import android.provider.Settings import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -18,9 +17,9 @@ 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.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -31,7 +30,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -48,7 +46,9 @@ import com.bitwarden.authenticator.R import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect import com.bitwarden.authenticator.ui.platform.base.util.asText +import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar +import com.bitwarden.authenticator.ui.platform.components.appbar.action.BitwardenSearchActionItem import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog @@ -57,8 +57,6 @@ import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoBut import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton -import com.bitwarden.authenticator.ui.platform.components.icon.BitwardenIcon -import com.bitwarden.authenticator.ui.platform.components.model.IconData import com.bitwarden.authenticator.ui.platform.components.model.IconResource import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme @@ -155,136 +153,81 @@ fun ItemListingScreen( }, ) - BitwardenScaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - BitwardenTopAppBar( - title = stringResource(id = R.string.verification_codes), - scrollBehavior = scrollBehavior, - navigationIcon = null, - actions = { - if (state.viewState !is ItemListingState.ViewState.NoItems) { - BitwardenIcon( - modifier = Modifier.clickable { - viewModel.trySendAction(ItemListingAction.SearchClick) - }, - iconData = IconData.Local(R.drawable.ic_search_24px), - tint = MaterialTheme.colorScheme.surfaceTint + when (val currentState = state.viewState) { + is ItemListingState.ViewState.Content -> { + ItemListingContent( + currentState, + scrollBehavior, + onNavigateToSearch = remember(viewModel) { + { + viewModel.trySendAction( + ItemListingAction.SearchClick + ) + } + }, + onScanQrCodeClick = remember(viewModel) { + { + launcher.launch(Manifest.permission.CAMERA) + } + }, + onEnterSetupKeyClick = remember(viewModel) { + { + viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) + } + }, + onItemClick = remember(viewModel) { + { + viewModel.trySendAction( + ItemListingAction.ItemClick(it) + ) + } + }, + onEditItemClick = remember(viewModel) { + { + viewModel.trySendAction( + ItemListingAction.EditItemClick(it) + ) + } + }, + onDeleteItemClick = remember(viewModel) { + { + viewModel.trySendAction( + ItemListingAction.DeleteItemClick(it) ) } } ) - }, - floatingActionButton = { - ExpandableFloatingActionButton( - modifier = Modifier - .semantics { testTag = "AddItemButton" } - .padding(bottom = 16.dp), - 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 = { - launcher.launch(Manifest.permission.CAMERA) - } - ), - ItemListingExpandableFabAction.EnterSetupKey( - label = R.string.enter_a_setup_key.asText(), - icon = IconResource( - iconPainter = painterResource(id = R.drawable.ic_keyboard_24px), - 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, - ), + } + + is ItemListingState.ViewState.Error -> { + Text( + text = "Error! ${currentState.message}", + modifier = Modifier.fillMaxSize(), ) - }, - floatingActionButtonPosition = FabPosition.EndOverlay, - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - ) { - when (val currentState = state.viewState) { - is ItemListingState.ViewState.Content -> { - LazyColumn { - items(currentState.itemList) { - VaultVerificationCodeItem( - authCode = it.authCode, - name = it.issuer, - username = it.username, - periodSeconds = it.periodSeconds, - timeLeftSeconds = it.timeLeftSeconds, - alertThresholdSeconds = it.alertThresholdSeconds, - startIcon = it.startIcon, - onItemClick = remember(viewModel) { - { - viewModel.trySendAction( - ItemListingAction.ItemClick(it.authCode) - ) - } - }, - onEditItemClick = remember(viewModel) { - { - viewModel.trySendAction( - ItemListingAction.EditItemClick(it.id) - ) - } - }, - onDeleteItemClick = remember(viewModel) { - { - viewModel.trySendAction( - ItemListingAction.DeleteItemClick(it.id) - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } + } + + ItemListingState.ViewState.Loading, + ItemListingState.ViewState.NoItems, + -> { + EmptyItemListingContent( + appTheme = state.appTheme, + scrollBehavior = scrollBehavior, + onAddCodeClick = remember(viewModel) { + { + launcher.launch(Manifest.permission.CAMERA) + } + }, + onScanQuCodeClick = remember(viewModel) { + { + launcher.launch(Manifest.permission.CAMERA) + } + }, + onEnterSetupKeyClick = remember(viewModel) { + { + viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) } } - - is ItemListingState.ViewState.Error -> { - Text( - text = "Error! ${currentState.message}", - modifier = Modifier.fillMaxSize(), - ) - } - - ItemListingState.ViewState.NoItems, - ItemListingState.ViewState.Loading, - -> { - EmptyItemListingContent( - appTheme = state.appTheme, - onAddCodeClick = remember(viewModel) { - { - launcher.launch(Manifest.permission.CAMERA) - } - }, - ) - } - } + ) } } } @@ -332,63 +275,218 @@ private fun ItemListingDialogs( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ItemListingContent( + state: ItemListingState.ViewState.Content, + scrollBehavior: TopAppBarScrollBehavior, + onNavigateToSearch: () -> Unit, + onScanQrCodeClick: () -> Unit, + onEnterSetupKeyClick: () -> Unit, + onItemClick: (String) -> Unit, + onEditItemClick: (String) -> Unit, + onDeleteItemClick: (String) -> Unit, +) { + BitwardenScaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + BitwardenMediumTopAppBar( + title = stringResource(id = R.string.verification_codes), + scrollBehavior = scrollBehavior, + actions = { + BitwardenSearchActionItem( + contentDescription = stringResource(id = R.string.search_codes), + onClick = onNavigateToSearch, + ) + } + ) + }, + floatingActionButton = { + ExpandableFloatingActionButton( + modifier = Modifier + .semantics { testTag = "AddItemButton" } + .padding(bottom = 16.dp), + 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 = onScanQrCodeClick, + ), + ItemListingExpandableFabAction.EnterSetupKey( + label = R.string.enter_a_setup_key.asText(), + icon = IconResource( + iconPainter = painterResource(id = R.drawable.ic_keyboard_24px), + contentDescription = stringResource(id = R.string.enter_a_setup_key), + testTag = "EnterSetupKeyButton", + ), + onEnterSetupKeyClick = onEnterSetupKeyClick + ) + ), + 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 -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + LazyColumn { + items(state.itemList) { + VaultVerificationCodeItem( + authCode = it.authCode, + name = it.issuer, + username = it.username, + periodSeconds = it.periodSeconds, + timeLeftSeconds = it.timeLeftSeconds, + alertThresholdSeconds = it.alertThresholdSeconds, + startIcon = it.startIcon, + onItemClick = { onItemClick(it.authCode) }, + onEditItemClick = { onEditItemClick(it.id) }, + onDeleteItemClick = { onDeleteItemClick(it.id) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + } + } +} + /** * Displays the item listing screen with no existing items. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun EmptyItemListingContent( modifier: Modifier = Modifier, appTheme: AppTheme, - onAddCodeClick: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( + rememberTopAppBarState() + ), + onAddCodeClick: () -> Unit, + onScanQuCodeClick: () -> Unit, + onEnterSetupKeyClick: () -> Unit, ) { - Column( - modifier = modifier + BitwardenScaffold( + modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + BitwardenTopAppBar( + title = stringResource(id = R.string.verification_codes), + scrollBehavior = scrollBehavior, + navigationIcon = null, + actions = { } + ) + }, + floatingActionButton = { + ExpandableFloatingActionButton( + modifier = Modifier + .semantics { testTag = "AddItemButton" } + .padding(bottom = 16.dp), + 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 = onScanQuCodeClick + ), + ItemListingExpandableFabAction.EnterSetupKey( + label = R.string.enter_a_setup_key.asText(), + icon = IconResource( + iconPainter = painterResource(id = R.drawable.ic_keyboard_24px), + contentDescription = stringResource(id = R.string.enter_a_setup_key), + testTag = "EnterSetupKeyButton", + ), + onEnterSetupKeyClick = onEnterSetupKeyClick, + ) + ), + 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, ) { - Image( - modifier = Modifier.fillMaxWidth(), - painter = painterResource( - id = when (appTheme) { - AppTheme.DARK -> R.drawable.ic_empty_vault_dark - AppTheme.LIGHT -> R.drawable.ic_empty_vault_light - AppTheme.DEFAULT -> R.drawable.ic_empty_vault - } - ), - contentDescription = stringResource( - id = R.string.empty_item_list, - ), - contentScale = ContentScale.Fit, - ) + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.fillMaxWidth(), + painter = painterResource( + id = when (appTheme) { + AppTheme.DARK -> R.drawable.ic_empty_vault_dark + AppTheme.LIGHT -> R.drawable.ic_empty_vault_light + AppTheme.DEFAULT -> R.drawable.ic_empty_vault + } + ), + contentDescription = stringResource( + id = R.string.empty_item_list, + ), + contentScale = ContentScale.Fit, + ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.you_dont_have_items_to_display), - style = Typography.titleMedium, - ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.you_dont_have_items_to_display), + style = Typography.titleMedium, + ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - textAlign = TextAlign.Center, - text = stringResource(id = R.string.empty_item_list_instruction), - ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + textAlign = TextAlign.Center, + text = stringResource(id = R.string.empty_item_list_instruction), + ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenFilledTonalButton( - modifier = Modifier.fillMaxWidth(), - label = stringResource(R.string.add_code), - onClick = onAddCodeClick, - ) + Spacer(modifier = Modifier.height(16.dp)) + BitwardenFilledTonalButton( + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.add_code), + onClick = onAddCodeClick, + ) + } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable @Preview(showBackground = true) fun EmptyListingContentPreview() { EmptyItemListingContent( - appTheme = AppTheme.DEFAULT, modifier = Modifier.padding(horizontal = 16.dp), + appTheme = AppTheme.DEFAULT, + onAddCodeClick = { }, + onScanQuCodeClick = { }, + onEnterSetupKeyClick = { }, ) } diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/appbar/action/BitwardenSearchActionItem.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/appbar/action/BitwardenSearchActionItem.kt new file mode 100644 index 0000000000..dc5cba9026 --- /dev/null +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/appbar/action/BitwardenSearchActionItem.kt @@ -0,0 +1,45 @@ +package com.bitwarden.authenticator.ui.platform.components.appbar.action + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import com.bitwarden.authenticator.R +import com.bitwarden.authenticator.ui.platform.components.util.rememberVectorPainter + +/** + * Represents the Bitwarden search action item. + * + * This is an [Icon] composable tailored specifically for the search functionality + * in the Bitwarden app. + * It presents the search icon and offers an `onClick` callback for when the icon is tapped. + * + * @param contentDescription A description of the UI element, used for accessibility purposes. + * @param onClick A callback to be invoked when this action item is clicked. + */ +@Composable +fun BitwardenSearchActionItem( + contentDescription: String, + onClick: () -> Unit, +) { + IconButton( + onClick = onClick, + modifier = Modifier.testTag("SearchButton"), + ) { + Icon( + painter = rememberVectorPainter(id = R.drawable.ic_search_24px), + contentDescription = contentDescription, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BitwardenSearchActionItem_preview() { + BitwardenSearchActionItem( + contentDescription = "Search", + onClick = {}, + ) +} diff --git a/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/util/RememberVectorPainter.kt b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/util/RememberVectorPainter.kt new file mode 100644 index 0000000000..1f63459471 --- /dev/null +++ b/app/src/main/kotlin/com/bitwarden/authenticator/ui/platform/components/util/RememberVectorPainter.kt @@ -0,0 +1,19 @@ +package com.bitwarden.authenticator.ui.platform.components.util + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.VectorPainter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource + +/** + * Returns a [VectorPainter] built from the given [id] to circumvent issues with painter resources + * recomposing unnecessarily. + */ +@Composable +fun rememberVectorPainter( + @DrawableRes id: Int, +): VectorPainter = rememberVectorPainter( + image = ImageVector.vectorResource(id), +)