Refactor ItemHeader to use LazyColumn and Crossfade for smoother transitions

- Migrates `ItemHeader` to `LazyColumn` to improve performance.
- Introduces `Crossfade` for animating title changes in `BitwardenExpandingHeader`.
- Adjusts icon sizing in `ItemHeaderIcon`.
- Removes unnecessary column scope and animated visibility from `ExpandingItemLocationContent`.
- Refactors to use `LazyItemScope` and adds `animateItem()` to `ItemLocationListItem`.
- Adds conditional handling for expanding the item locations list.
This commit is contained in:
Patrick Honkonen
2025-02-25 17:35:02 -05:00
committed by David Perez
parent 0ba240852f
commit d7d099477f
7 changed files with 1193 additions and 1229 deletions

View File

@@ -1,6 +1,9 @@
package com.x8bit.bitwarden.ui.platform.components.header
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@@ -50,23 +53,46 @@ fun BitwardenExpandingHeader(
.semantics(mergeDescendants = true) {},
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = if (isExpanded) expandedText else collapsedText,
color = BitwardenTheme.colorScheme.text.interaction,
style = BitwardenTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp),
)
if (showExpansionIndicator) {
val iconRotationDegrees = animateFloatAsState(
targetValue = if (isExpanded) 0f else 180f,
label = "expanderIconRotationAnimation",
)
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_up_small),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.rotate(degrees = iconRotationDegrees.value),
)
Crossfade(
targetState = isExpanded,
label = "BitwardenExpandingHeaderTitle_animation",
// Make the animation shorter when the text is the same to avoid crossfading the same
// text.
animationSpec = tween(
durationMillis = if (expandedText != collapsedText) {
AnimationConstants.DefaultDurationMillis
} else {
0
},
),
) { expanded ->
if (expanded) {
Text(
text = expandedText,
color = BitwardenTheme.colorScheme.text.interaction,
style = BitwardenTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp),
)
} else {
Text(
text = collapsedText,
color = BitwardenTheme.colorScheme.text.interaction,
style = BitwardenTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp),
)
}
if (showExpansionIndicator) {
val iconRotationDegrees = animateFloatAsState(
targetValue = if (isExpanded) 0f else 180f,
label = "expanderIconRotationAnimation",
)
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_up_small),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.rotate(degrees = iconRotationDegrees.value),
)
}
}
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -41,253 +42,254 @@ fun VaultItemCardContent(
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "CardItemNameIcon",
textFieldTestTag = "CardItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
cardState.cardholderName?.let { cardholderName ->
item {
BitwardenTextField(
label = stringResource(id = R.string.cardholder_name),
value = cardholderName,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardholderNameEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = cardholderName),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Column(modifier = modifier) {
Spacer(Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "CardItemNameIcon",
textFieldTestTag = "CardItemNameEntry",
)
LazyColumn(modifier = modifier) {
cardState.cardholderName?.let { cardholderName ->
item {
BitwardenTextField(
label = stringResource(id = R.string.cardholder_name),
value = cardholderName,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardholderNameEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = cardholderName),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
cardState.number?.let { numberData ->
item {
BitwardenPasswordField(
label = stringResource(id = R.string.number),
value = numberData.number,
onValueChange = {},
showPassword = numberData.isVisible,
showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick,
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_number),
onClick = vaultCardItemTypeHandlers.onCopyNumberClick,
modifier = Modifier.testTag(tag = "CardCopyNumberButton"),
)
},
passwordFieldTestTag = "CardNumberEntry",
showPasswordTestTag = "CardViewNumberButton",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = numberData),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
cardState.number?.let { numberData ->
item {
BitwardenPasswordField(
label = stringResource(id = R.string.number),
value = numberData.number,
onValueChange = {},
showPassword = numberData.isVisible,
showPasswordChange = vaultCardItemTypeHandlers.onShowNumberClick,
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_number),
onClick = vaultCardItemTypeHandlers.onCopyNumberClick,
modifier = Modifier.testTag(tag = "CardCopyNumberButton"),
)
},
passwordFieldTestTag = "CardNumberEntry",
showPasswordTestTag = "CardViewNumberButton",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = numberData),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) {
item {
BitwardenTextField(
label = stringResource(id = R.string.brand),
value = cardState.brand.shortName(),
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardBrandEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = cardState.brand),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) {
item {
BitwardenTextField(
label = stringResource(id = R.string.brand),
value = cardState.brand.shortName(),
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardBrandEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = cardState.brand),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
cardState.expiration?.let { expiration ->
item {
BitwardenTextField(
label = stringResource(id = R.string.expiration),
value = expiration,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardExpirationEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = expiration),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
cardState.expiration?.let { expiration ->
item {
BitwardenTextField(
label = stringResource(id = R.string.expiration),
value = expiration,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardExpirationEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = expiration),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
cardState.securityCode?.let { securityCodeData ->
item {
BitwardenPasswordField(
label = stringResource(id = R.string.security_code),
value = securityCodeData.code,
onValueChange = {},
showPassword = securityCodeData.isVisible,
showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick,
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_security_code),
onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick,
modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"),
)
},
showPasswordTestTag = "CardViewSecurityCodeButton",
passwordFieldTestTag = "CardSecurityCodeEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = securityCodeData),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
cardState.securityCode?.let { securityCodeData ->
item {
BitwardenPasswordField(
label = stringResource(id = R.string.security_code),
value = securityCodeData.code,
onValueChange = {},
showPassword = securityCodeData.isVisible,
showPasswordChange = vaultCardItemTypeHandlers.onShowSecurityCodeClick,
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(
id = R.string.copy_security_code,
),
onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick,
modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"),
)
},
showPasswordTestTag = "CardViewSecurityCodeButton",
passwordFieldTestTag = "CardSecurityCodeEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = securityCodeData),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField =
vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField =
vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick =
vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
.padding(horizontal = 12.dp),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -38,303 +39,302 @@ fun VaultItemIdentityContent(
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "IdentityItemNameIcon",
textFieldTestTag = "IdentityItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
identityState.identityName?.let { identityName ->
item {
IdentityCopyField(
label = stringResource(id = R.string.identity_name),
value = identityName,
copyContentDescription = stringResource(id = R.string.copy_identity_name),
textFieldTestTag = "IdentityNameEntry",
copyActionTestTag = "IdentityCopyNameButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = identityName),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Column(modifier = modifier) {
Spacer(Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "IdentityItemNameIcon",
textFieldTestTag = "IdentityItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
LazyColumn {
identityState.identityName?.let { identityName ->
item {
IdentityCopyField(
label = stringResource(id = R.string.identity_name),
value = identityName,
copyContentDescription = stringResource(id = R.string.copy_identity_name),
textFieldTestTag = "IdentityNameEntry",
copyActionTestTag = "IdentityCopyNameButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = identityName),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.username?.let { username ->
item {
IdentityCopyField(
label = stringResource(id = R.string.username),
value = username,
copyContentDescription = stringResource(id = R.string.copy_username),
textFieldTestTag = "IdentityUsernameEntry",
copyActionTestTag = "IdentityCopyUsernameButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = username),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.username?.let { username ->
item {
IdentityCopyField(
label = stringResource(id = R.string.username),
value = username,
copyContentDescription = stringResource(id = R.string.copy_username),
textFieldTestTag = "IdentityUsernameEntry",
copyActionTestTag = "IdentityCopyUsernameButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = username),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.company?.let { company ->
item {
IdentityCopyField(
label = stringResource(id = R.string.company),
value = company,
copyContentDescription = stringResource(id = R.string.copy_company),
textFieldTestTag = "IdentityCompanyEntry",
copyActionTestTag = "IdentityCopyCompanyButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = company),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.company?.let { company ->
item {
IdentityCopyField(
label = stringResource(id = R.string.company),
value = company,
copyContentDescription = stringResource(id = R.string.copy_company),
textFieldTestTag = "IdentityCompanyEntry",
copyActionTestTag = "IdentityCopyCompanyButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = company),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.ssn?.let { ssn ->
item {
IdentityCopyField(
label = stringResource(id = R.string.ssn),
value = ssn,
copyContentDescription = stringResource(id = R.string.copy_ssn),
textFieldTestTag = "IdentitySsnEntry",
copyActionTestTag = "IdentityCopySsnButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = ssn),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.ssn?.let { ssn ->
item {
IdentityCopyField(
label = stringResource(id = R.string.ssn),
value = ssn,
copyContentDescription = stringResource(id = R.string.copy_ssn),
textFieldTestTag = "IdentitySsnEntry",
copyActionTestTag = "IdentityCopySsnButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = ssn),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.passportNumber?.let { passportNumber ->
item {
IdentityCopyField(
label = stringResource(id = R.string.passport_number),
value = passportNumber,
copyContentDescription = stringResource(id = R.string.copy_passport_number),
textFieldTestTag = "IdentityPassportNumberEntry",
copyActionTestTag = "IdentityCopyPassportNumberButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = passportNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.passportNumber?.let { passportNumber ->
item {
IdentityCopyField(
label = stringResource(id = R.string.passport_number),
value = passportNumber,
copyContentDescription = stringResource(id = R.string.copy_passport_number),
textFieldTestTag = "IdentityPassportNumberEntry",
copyActionTestTag = "IdentityCopyPassportNumberButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = passportNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.licenseNumber?.let { licenseNumber ->
item {
IdentityCopyField(
label = stringResource(id = R.string.license_number),
value = licenseNumber,
copyContentDescription = stringResource(id = R.string.copy_license_number),
textFieldTestTag = "IdentityLicenseNumberEntry",
copyActionTestTag = "IdentityCopyLicenseNumberButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = licenseNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.licenseNumber?.let { licenseNumber ->
item {
IdentityCopyField(
label = stringResource(id = R.string.license_number),
value = licenseNumber,
copyContentDescription = stringResource(id = R.string.copy_license_number),
textFieldTestTag = "IdentityLicenseNumberEntry",
copyActionTestTag = "IdentityCopyLicenseNumberButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = licenseNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.email?.let { email ->
item {
IdentityCopyField(
label = stringResource(id = R.string.email),
value = email,
copyContentDescription = stringResource(id = R.string.copy_email),
textFieldTestTag = "IdentityEmailEntry",
copyActionTestTag = "IdentityCopyEmailButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = email),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.email?.let { email ->
item {
IdentityCopyField(
label = stringResource(id = R.string.email),
value = email,
copyContentDescription = stringResource(id = R.string.copy_email),
textFieldTestTag = "IdentityEmailEntry",
copyActionTestTag = "IdentityCopyEmailButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = email),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.phone?.let { phone ->
item {
IdentityCopyField(
label = stringResource(id = R.string.phone),
value = phone,
copyContentDescription = stringResource(id = R.string.copy_phone),
textFieldTestTag = "IdentityPhoneEntry",
copyActionTestTag = "IdentityCopyPhoneButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = phone),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.phone?.let { phone ->
item {
IdentityCopyField(
label = stringResource(id = R.string.phone),
value = phone,
copyContentDescription = stringResource(id = R.string.copy_phone),
textFieldTestTag = "IdentityPhoneEntry",
copyActionTestTag = "IdentityCopyPhoneButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = phone),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
identityState.address?.let { address ->
item {
IdentityCopyField(
label = stringResource(id = R.string.address),
value = address,
copyContentDescription = stringResource(id = R.string.copy_address),
textFieldTestTag = "IdentityAddressEntry",
copyActionTestTag = "IdentityCopyAddressButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = address),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
identityState.address?.let { address ->
item {
IdentityCopyField(
label = stringResource(id = R.string.address),
value = address,
copyContentDescription = stringResource(id = R.string.copy_address),
textFieldTestTag = "IdentityAddressEntry",
copyActionTestTag = "IdentityCopyAddressButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = address),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
}
commonState.notes?.let { notes ->
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.notes),
value = notes,
copyContentDescription = stringResource(id = R.string.copy_notes),
textFieldTestTag = "CipherNotesLabel",
copyActionTestTag = "CipherNotesCopyButton",
onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.notes),
value = notes,
copyContentDescription = stringResource(id = R.string.copy_notes),
textFieldTestTag = "CipherNotesLabel",
copyActionTestTag = "CipherNotesCopyButton",
onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
.padding(horizontal = 12.dp),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -50,246 +51,229 @@ fun VaultItemLoginContent(
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
Column(
modifier = modifier
.fillMaxWidth()
.standardHorizontalMargin(),
) {
item {
Spacer(Modifier.height(12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "LoginItemNameIcon",
textFieldTestTag = "LoginItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
Spacer(Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "LoginItemNameIcon",
textFieldTestTag = "LoginItemNameEntry",
)
LazyColumn(
modifier = modifier,
) {
if (loginItemState.hasLoginCredentials) {
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.login_credentials),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
loginItemState.username?.let { username ->
item {
UsernameField(
username = username,
onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick,
cardStyle = loginItemState
.passwordData
?.let { CardStyle.Top(dividerPadding = 0.dp) }
?: CardStyle.Full,
modifier = Modifier
.fillMaxWidth(),
)
}
}
loginItemState.passwordData?.let { passwordData ->
item {
PasswordField(
passwordData = passwordData,
onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick,
onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick,
onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick,
cardStyle = loginItemState
.username
?.let { CardStyle.Bottom }
?: CardStyle.Full,
modifier = Modifier
.fillMaxWidth(),
)
}
}
loginItemState.fido2CredentialCreationDateText?.let { creationDate ->
item {
Spacer(modifier = Modifier.height(8.dp))
Fido2CredentialField(
creationDate = creationDate(),
modifier = Modifier
.fillMaxWidth(),
)
}
}
loginItemState.totpCodeItemData?.let { totpCodeItemData ->
item {
Spacer(modifier = Modifier.height(8.dp))
TotpField(
totpCodeItemData = totpCodeItemData,
enabled = loginItemState.canViewTotpCode,
onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick,
onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers
.onAuthenticatorHelpToolTipClick,
modifier = Modifier
.fillMaxWidth(),
)
}
}
loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.autofill_options),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(uris) { index, uriData ->
UriField(
uriData = uriData,
onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick,
onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick,
cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp),
modifier = Modifier
.fillMaxWidth(),
)
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
NotesField(
notes = notes,
onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier
.fillMaxWidth(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth(),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField =
vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField =
vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick =
vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth(),
attachmentItem = attachmentItem,
cardStyle = attachments.toListItemCardStyle(index = index),
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
)
}
}
if (loginItemState.hasLoginCredentials) {
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.login_credentials),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
loginItemState.username?.let { username ->
item {
UsernameField(
username = username,
onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick,
cardStyle = loginItemState
.passwordData
?.let { CardStyle.Top(dividerPadding = 0.dp) }
?: CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
loginItemState.passwordData?.let { passwordData ->
item {
PasswordField(
passwordData = passwordData,
onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick,
onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick,
onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick,
cardStyle = loginItemState
.username
?.let { CardStyle.Bottom }
?: CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
loginItemState.fido2CredentialCreationDateText?.let { creationDate ->
item {
Spacer(modifier = Modifier.height(8.dp))
Fido2CredentialField(
creationDate = creationDate(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
loginItemState.totpCodeItemData?.let { totpCodeItemData ->
item {
Spacer(modifier = Modifier.height(8.dp))
TotpField(
totpCodeItemData = totpCodeItemData,
enabled = loginItemState.canViewTotpCode,
onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick,
onAuthenticatorHelpToolTipClick = vaultLoginItemTypeHandlers
.onAuthenticatorHelpToolTipClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.autofill_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(uris) { index, uriData ->
UriField(
uriData = uriData,
onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick,
onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick,
cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
NotesField(
notes = notes,
onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
cardStyle = attachments.toListItemCardStyle(index = index),
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
)
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
loginItemState.passwordRevisionDate?.let { revisionDate ->
item {
Spacer(modifier = Modifier.height(height = 4.dp))
Spacer(modifier = Modifier.height(16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_password_updated)}: ",
text = revisionDate,
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
.fillMaxWidth(),
)
}
}
loginItemState.passwordHistoryCount?.let { passwordHistoryCount ->
loginItemState.passwordRevisionDate?.let { revisionDate ->
item {
Spacer(modifier = Modifier.height(height = 4.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_password_updated)}: ",
text = revisionDate,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
)
}
}
loginItemState.passwordHistoryCount?.let { passwordHistoryCount ->
item {
Spacer(modifier = Modifier.height(height = 4.dp))
BitwardenHyperTextLink(
annotatedResId = R.string.password_history_count,
args = arrayOf(passwordHistoryCount.toString()),
annotationKey = "passwordHistory",
accessibilityString = stringResource(id = R.string.password_history),
onClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick,
style = BitwardenTheme.typography.labelMedium,
modifier = Modifier
.wrapContentWidth()
.padding(horizontal = 12.dp),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 4.dp))
BitwardenHyperTextLink(
annotatedResId = R.string.password_history_count,
args = arrayOf(passwordHistoryCount.toString()),
annotationKey = "passwordHistory",
accessibilityString = stringResource(id = R.string.password_history),
onClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick,
style = BitwardenTheme.typography.labelMedium,
modifier = Modifier
.wrapContentWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -38,126 +39,129 @@ fun VaultItemSecureNoteContent(
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "SecureNoteItemNameIcon",
textFieldTestTag = "SecureNoteItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
Column(modifier = modifier) {
Spacer(Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "SecureNoteItemNameIcon",
textFieldTestTag = "SecureNoteItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
LazyColumn {
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField =
vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField =
vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick =
vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
Row(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
.padding(horizontal = 12.dp)
.semantics(mergeDescendants = true) { },
) {
Text(
text = "${stringResource(id = R.string.date_updated)}: ",
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Text(
text = commonState.lastUpdated,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.semantics(mergeDescendants = true) { },
) {
Text(
text = "${stringResource(id = R.string.date_updated)}: ",
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Text(
text = commonState.lastUpdated,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
}
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -39,207 +40,195 @@ fun VaultItemSshKeyContent(
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "SshKeyItemNameIcon",
textFieldTestTag = "SshKeyItemNameEntry",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.public_key),
value = sshKeyItemState.publicKey,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_public_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"),
)
},
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("SshKeyItemPublicKeyEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenPasswordField(
label = stringResource(id = R.string.private_key),
value = sshKeyItemState.privateKey,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_private_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"),
)
},
showPassword = sshKeyItemState.showPrivateKey,
showPasswordTestTag = "ViewPrivateKeyButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("SshKeyItemPrivateKeyEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = R.string.fingerprint),
value = sshKeyItemState.fingerprint,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_fingerprint),
onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick,
modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"),
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("SshKeyItemFingerprintEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
commonState.notes?.let { notes ->
Column(modifier = modifier) {
Spacer(Modifier.height(height = 12.dp))
ItemHeader(
value = commonState.name,
isFavorite = commonState.favorite,
iconData = commonState.iconData,
relatedLocations = commonState.relatedLocations,
iconTestTag = "SshKeyItemNameIcon",
textFieldTestTag = "SshKeyItemNameEntry",
)
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
value = notes,
label = stringResource(id = R.string.public_key),
value = sshKeyItemState.publicKey,
onValueChange = { },
readOnly = true,
singleLine = false,
readOnly = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
contentDescription = stringResource(id = R.string.copy_public_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("SshKeyItemPublicKeyEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
BitwardenPasswordField(
label = stringResource(id = R.string.private_key),
value = sshKeyItemState.privateKey,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_private_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPrivateKeyButton"),
)
},
showPassword = sshKeyItemState.showPrivateKey,
showPasswordTestTag = "ViewPrivateKeyButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("SshKeyItemPrivateKeyEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
BitwardenTextField(
label = stringResource(id = R.string.fingerprint),
value = sshKeyItemState.fingerprint,
onValueChange = { },
singleLine = false,
readOnly = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_fingerprint),
onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick,
modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"),
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("SshKeyItemFingerprintEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
value = notes,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
modifier = Modifier.testTag(tag = "CipherNotesCopyButton"),
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField =
vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField =
vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick =
vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
.padding(horizontal = 12.dp)
.testTag("SshKeyItemLastUpdated"),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.testTag("SshKeyItemLastUpdated"),
)
}
item {
Spacer(modifier = Modifier.height(88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View File

@@ -1,23 +1,18 @@
package com.x8bit.bitwarden.ui.vault.feature.item.component
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -28,7 +23,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.draw.clipToBounds
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@@ -36,7 +30,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
@@ -50,7 +43,6 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* The max number of items that can be displayed before the "show more" text is visible.
@@ -58,9 +50,8 @@ import kotlinx.collections.immutable.toImmutableList
private const val EXPANDABLE_THRESHOLD = 2
/**
* Reusable composable for displaying the cipher name and favorite status.
* Reusable composable for displaying the cipher name, favorite status, and related locations.
*/
@OmitFromCoverage
@Suppress("LongMethod")
@Composable
fun ItemHeader(
@@ -72,119 +63,107 @@ fun ItemHeader(
iconTestTag: String? = null,
textFieldTestTag: String? = null,
) {
Column(
var isExpanded by rememberSaveable { mutableStateOf(false) }
LazyColumn(
modifier = modifier
.cardStyle(CardStyle.Full)
.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
ItemHeaderIcon(
iconData = iconData,
testTag = iconTestTag,
modifier = Modifier.size(36.dp),
)
BitwardenTextField(
label = null,
value = value,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
Icon(
painter = painterResource(
id = if (isFavorite) {
R.drawable.ic_favorite_full
} else {
R.drawable.ic_favorite_empty
},
),
contentDescription = stringResource(
id = if (isFavorite) R.string.favorite else R.string.unfavorite,
),
modifier = Modifier.padding(all = 12.dp),
)
},
textFieldTestTag = textFieldTestTag,
cardStyle = null,
textStyle = BitwardenTheme.typography.titleMedium,
)
}
BitwardenHorizontalDivider(Modifier.padding(start = 16.dp))
Spacer(Modifier.height(8.dp))
if (relatedLocations.isEmpty()) {
ItemLocationListItem(
vectorPainter = rememberVectorPainter(R.drawable.ic_folder),
text = stringResource(R.string.no_folder),
iconTestTag = "NoFolderIcon",
item {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
return@Column
) {
ItemHeaderIcon(
iconData = iconData,
testTag = iconTestTag,
modifier = Modifier.size(36.dp),
)
BitwardenTextField(
label = null,
value = value,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
Icon(
painter = painterResource(
id = if (isFavorite) {
R.drawable.ic_favorite_full
} else {
R.drawable.ic_favorite_empty
},
),
contentDescription = stringResource(
id = if (isFavorite) R.string.favorite else R.string.unfavorite,
),
modifier = Modifier.padding(all = 12.dp),
)
},
textFieldTestTag = textFieldTestTag,
cardStyle = null,
textStyle = BitwardenTheme.typography.titleMedium,
)
}
BitwardenHorizontalDivider(Modifier.padding(start = 16.dp))
Spacer(Modifier.height(8.dp))
}
relatedLocations
.take(EXPANDABLE_THRESHOLD)
.forEach {
if (relatedLocations.isEmpty()) {
item {
ItemLocationListItem(
vectorPainter = rememberVectorPainter(it.icon),
iconTestTag = "ItemLocationIcon",
text = it.name,
vectorPainter = rememberVectorPainter(R.drawable.ic_folder),
text = stringResource(R.string.no_folder),
iconTestTag = "NoFolderIcon",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
return@LazyColumn
}
ExpandingItemLocationContent(
overflowLocations = relatedLocations
.drop(EXPANDABLE_THRESHOLD)
.toImmutableList(),
)
}
}
items(relatedLocations.take(EXPANDABLE_THRESHOLD)) {
ItemLocationListItem(
vectorPainter = rememberVectorPainter(it.icon),
iconTestTag = "ItemLocationIcon",
text = it.name,
modifier = Modifier
.animateItem()
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
@Composable
private fun ColumnScope.ExpandingItemLocationContent(
overflowLocations: ImmutableList<VaultItemLocation>,
) {
var isExpanded by rememberSaveable { mutableStateOf(false) }
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
modifier = Modifier.clipToBounds(),
) {
LazyColumn {
items(overflowLocations) {
if (isExpanded) {
items(relatedLocations.drop(EXPANDABLE_THRESHOLD)) {
ItemLocationListItem(
vectorPainter = rememberVectorPainter(it.icon),
text = it.name,
iconTestTag = "ItemLocationIcon",
modifier = Modifier
.padding(horizontal = 16.dp)
.animateItem()
.fillMaxWidth(),
)
}
}
}
if (overflowLocations.isNotEmpty()) {
BitwardenExpandingHeader(
collapsedText = stringResource(R.string.show_more),
expandedText = stringResource(R.string.show_less),
isExpanded = isExpanded,
onClick = { isExpanded = !isExpanded },
showExpansionIndicator = false,
)
if (relatedLocations.size > EXPANDABLE_THRESHOLD) {
item {
BitwardenExpandingHeader(
collapsedText = stringResource(R.string.show_more),
expandedText = stringResource(R.string.show_less),
isExpanded = isExpanded,
onClick = { isExpanded = !isExpanded },
showExpansionIndicator = false,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}
}
}
@@ -194,31 +173,35 @@ private fun ItemHeaderIcon(
modifier: Modifier = Modifier,
testTag: String? = null,
) {
val isLocalIcon = iconData is IconData.Local
Box(
contentAlignment = Alignment.Center,
modifier = if (iconData is IconData.Local) {
modifier.then(
modifier = modifier.then(
if (isLocalIcon) {
Modifier.background(
color = BitwardenTheme.colorScheme.illustration.backgroundPrimary,
shape = BitwardenTheme.shapes.favicon,
),
)
} else {
modifier
},
)
} else {
Modifier
},
),
) {
BitwardenIcon(
iconData = iconData,
contentDescription = null,
tint = BitwardenTheme.colorScheme.illustration.outline,
modifier = Modifier
.nullableTestTag(testTag),
.nullableTestTag(testTag)
.then(
if (!isLocalIcon) Modifier.fillMaxSize() else Modifier,
),
)
}
}
@Composable
private fun ItemLocationListItem(
private fun LazyItemScope.ItemLocationListItem(
vectorPainter: VectorPainter,
iconTestTag: String?,
text: String,
@@ -254,18 +237,14 @@ private fun ItemLocationListItem(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ItemHeader_LocalIcon_Preview() {
BitwardenTheme {
LazyColumn {
item {
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(),
)
}
}
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(),
)
}
}
@@ -273,19 +252,15 @@ private fun ItemHeader_LocalIcon_Preview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ItemHeader_NetworkIcon_Preview() {
BitwardenTheme {
LazyColumn {
item {
ItemHeader(
value = "Login with favicon",
isFavorite = true,
iconData = IconData.Network(
uri = "mockuri",
fallbackIconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(),
)
}
}
ItemHeader(
value = "Login with favicon",
isFavorite = true,
iconData = IconData.Network(
uri = "mockuri",
fallbackIconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(),
)
}
}
@@ -293,20 +268,16 @@ private fun ItemHeader_NetworkIcon_Preview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun ItemHeader_Organization_Preview() {
BitwardenTheme {
LazyColumn {
item {
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
),
)
}
}
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
),
)
}
}
@@ -314,21 +285,17 @@ private fun ItemHeader_Organization_Preview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ItemNameField_Org_SingleCollection_Preview() {
BitwardenTheme {
LazyColumn {
item {
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
VaultItemLocation.Collection("Marketing"),
),
)
}
}
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
VaultItemLocation.Collection("Marketing"),
),
)
}
}
@@ -336,22 +303,18 @@ private fun ItemNameField_Org_SingleCollection_Preview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ItemNameField_Org_MultiCollection_Preview() {
BitwardenTheme {
LazyColumn {
item {
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
VaultItemLocation.Collection("Marketing"),
VaultItemLocation.Collection("Product"),
),
)
}
}
ItemHeader(
value = "Login without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_globe,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
VaultItemLocation.Collection("Marketing"),
VaultItemLocation.Collection("Product"),
),
)
}
}
@@ -359,22 +322,18 @@ private fun ItemNameField_Org_MultiCollection_Preview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ItemNameField_Org_SingleCollection_Folder_Preview() {
BitwardenTheme {
LazyColumn {
item {
ItemHeader(
value = "Note without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_note,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
VaultItemLocation.Collection("Marketing"),
VaultItemLocation.Folder("Competition"),
),
)
}
}
ItemHeader(
value = "Note without favicon",
isFavorite = true,
iconData = IconData.Local(
iconRes = R.drawable.ic_note,
),
relatedLocations = persistentListOf(
VaultItemLocation.Organization("Stark Industries"),
VaultItemLocation.Collection("Marketing"),
VaultItemLocation.Folder("Competition"),
),
)
}
}
//endregion Previews