Adjust top app bar (#58)

This commit is contained in:
Patrick Honkonen
2024-04-26 11:02:18 -04:00
committed by GitHub
parent 2b6fe1b28a
commit 2bd0df2910
3 changed files with 327 additions and 165 deletions

View File

@@ -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 = { },
)
}

View File

@@ -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 = {},
)
}

View File

@@ -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),
)