diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt index 9b9bb90b7d..67e19e2f70 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt @@ -42,6 +42,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -167,6 +168,7 @@ private fun SetupAutoFillContent( ), isChecked = state.autofillEnabled, onCheckedChange = onAutofillServiceChanged, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt index bafabb3629..4a111dfb22 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt @@ -46,11 +46,13 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager +import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricSupportStatus import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.isPortrait @@ -156,11 +158,13 @@ private fun SetupUnlockScreenContent( } Spacer(modifier = Modifier.height(height = 24.dp)) + val biometricSupportStatus = biometricsManager.biometricSupportStatus BitwardenUnlockWithBiometricsSwitch( - biometricSupportStatus = biometricsManager.biometricSupportStatus, + biometricSupportStatus = biometricSupportStatus, isChecked = state.isUnlockWithBiometricsEnabled || showBiometricsPrompt, onDisableBiometrics = handler.onDisableBiometrics, onEnableBiometrics = handler.onEnableBiometrics, + cardStyle = CardStyle.Top(), modifier = Modifier .testTag(tag = "UnlockWithBiometricsSwitch") .fillMaxWidth() @@ -170,6 +174,11 @@ private fun SetupUnlockScreenContent( isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled, isUnlockWithPinEnabled = state.isUnlockWithPinEnabled, onUnlockWithPinToggleAction = handler.onUnlockWithPinToggle, + cardStyle = if (biometricSupportStatus == BiometricSupportStatus.NOT_SUPPORTED) { + CardStyle.Full + } else { + CardStyle.Bottom + }, modifier = Modifier .testTag(tag = "UnlockWithPinSwitch") .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt index cd56234722..932ee1b452 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt @@ -5,6 +5,7 @@ import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -52,6 +53,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -162,6 +164,7 @@ fun CompleteRegistrationScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) CompleteRegistrationContent( passwordInput = state.passwordInput, passwordStrengthState = state.passwordStrengthState, @@ -175,6 +178,7 @@ fun CompleteRegistrationScreen( showNewOnboardingUi = state.onboardingEnabled, userEmail = state.userEmail, ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -200,7 +204,6 @@ private fun CompleteRegistrationContent( modifier = modifier .fillMaxWidth(), ) { - Spacer(modifier = Modifier.height(8.dp)) if (showNewOnboardingUi) { CompleteRegistrationContentHeader( modifier = Modifier @@ -242,23 +245,22 @@ private fun CompleteRegistrationContent( showPasswordChange = { showPassword = it }, value = passwordInput, onValueChange = handler.onPasswordInputChange, - hint = stringResource(id = R.string.master_password_important_hint) - .takeIf { !showNewOnboardingUi }, + showPasswordTestTag = "PasswordVisibilityToggle", + imeAction = ImeAction.Next, + supportingTextContent = { + PasswordStrengthIndicator( + state = passwordStrengthState, + currentCharacterCount = passwordInput.length, + minimumCharacterCount = minimumPasswordLength.takeIf { showNewOnboardingUi }, + modifier = Modifier.fillMaxWidth(), + ) + }, + cardStyle = CardStyle.Top(dividerPadding = 0.dp), modifier = Modifier .testTag("MasterPasswordEntry") .fillMaxWidth() .standardHorizontalMargin(), - showPasswordTestTag = "PasswordVisibilityToggle", - imeAction = ImeAction.Next, ) - Spacer(modifier = Modifier.height(8.dp)) - PasswordStrengthIndicator( - state = passwordStrengthState, - currentCharacterCount = passwordInput.length, - minimumCharacterCount = minimumPasswordLength.takeIf { showNewOnboardingUi }, - modifier = Modifier.standardHorizontalMargin(), - ) - Spacer(modifier = Modifier.height(16.dp)) BitwardenPasswordField( label = stringResource( id = R.string.retype_master_password_required @@ -269,13 +271,13 @@ private fun CompleteRegistrationContent( showPassword = showPassword, showPasswordChange = { showPassword = it }, onValueChange = handler.onConfirmPasswordInputChange, + showPasswordTestTag = "ConfirmPasswordVisibilityToggle", + cardStyle = CardStyle.Middle(dividerPadding = 0.dp), modifier = Modifier .testTag("ConfirmMasterPasswordEntry") .fillMaxWidth() .standardHorizontalMargin(), - showPasswordTestTag = "ConfirmPasswordVisibilityToggle", ) - Spacer(modifier = Modifier.height(16.dp)) BitwardenTextField( label = stringResource( id = R.string.master_password_hint_not_specified @@ -284,37 +286,46 @@ private fun CompleteRegistrationContent( ), value = passwordHintInput, onValueChange = handler.onPasswordHintChange, - hint = stringResource( - id = R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password - .takeIf { showNewOnboardingUi } - ?: R.string.master_password_hint_description, - ), + supportingTextContent = { + Text( + text = stringResource( + id = R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password + .takeIf { showNewOnboardingUi } + ?: R.string.master_password_hint_description, + ), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + if (showNewOnboardingUi) { + BitwardenClickableText( + label = stringResource( + id = R.string.learn_about_other_ways_to_prevent_account_lockout, + ), + onClick = handler.onLearnToPreventLockout, + style = BitwardenTheme.typography.labelMedium, + innerPadding = PaddingValues(vertical = 4.dp), + ) + } + }, + textFieldTestTag = "MasterPasswordHintLabel", + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), - textFieldTestTag = "MasterPasswordHintLabel", ) - if (showNewOnboardingUi) { - BitwardenClickableText( - label = stringResource( - id = R.string.learn_about_other_ways_to_prevent_account_lockout, - ), - onClick = handler.onLearnToPreventLockout, - style = BitwardenTheme.typography.labelMedium, - modifier = Modifier.standardHorizontalMargin(), - ) - } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(id = R.string.check_known_data_breaches_for_this_password), isChecked = isCheckDataBreachesToggled, onCheckedChange = handler.onCheckDataBreachesToggle, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("CheckExposedMasterPasswordToggle") .standardHorizontalMargin(), ) if (showNewOnboardingUi) { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenFilledButton( label = callToActionText, isEnabled = nextButtonEnabled, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt index 7835542d42..332e1d5447 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer @@ -37,9 +38,9 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber") @Composable fun PasswordStrengthIndicator( - modifier: Modifier = Modifier, state: PasswordStrengthState, currentCharacterCount: Int, + modifier: Modifier = Modifier, minimumCharacterCount: Int? = null, ) { val minimumRequirementMet = (minimumCharacterCount == null) || @@ -86,6 +87,7 @@ fun PasswordStrengthIndicator( Modifier .fillMaxWidth() .height(4.dp) + .clip(shape = BitwardenTheme.shapes.progressIndicator) .background(BitwardenTheme.colorScheme.sliderButton.unfilled), ) { Box( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt index aa3dfa8735..ce423ebcac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt @@ -53,6 +53,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Ter import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog @@ -60,6 +61,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -179,43 +181,50 @@ fun CreateAccountScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenTextField( label = stringResource(id = R.string.email_address), value = state.emailInput, onValueChange = remember(viewModel) { { viewModel.trySendAction(EmailInputChange(it)) } }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), keyboardType = KeyboardType.Email, textFieldTestTag = "EmailAddressEntry", + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) var showPassword by rememberSaveable { mutableStateOf(false) } BitwardenPasswordField( label = stringResource(id = R.string.master_password), showPassword = showPassword, showPasswordChange = { showPassword = it }, value = state.passwordInput, - hint = state.passwordLengthLabel(), onValueChange = remember(viewModel) { { viewModel.trySendAction(PasswordInputChange(it)) } }, + showPasswordTestTag = "PasswordVisibilityToggle", + supportingTextContent = { + PasswordStrengthIndicator( + modifier = Modifier.fillMaxWidth(), + state = state.passwordStrengthState, + currentCharacterCount = state.passwordInput.length, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = state.passwordLengthLabel(), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + ) + }, + cardStyle = CardStyle.Top(dividerPadding = 0.dp), modifier = Modifier .testTag("MasterPasswordEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), - showPasswordTestTag = "PasswordVisibilityToggle", + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(8.dp)) - PasswordStrengthIndicator( - modifier = Modifier.padding(horizontal = 16.dp), - state = state.passwordStrengthState, - currentCharacterCount = state.passwordInput.length, - ) - Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordField( label = stringResource(id = R.string.retype_master_password), value = state.confirmPasswordInput, @@ -224,26 +233,27 @@ fun CreateAccountScreen( onValueChange = remember(viewModel) { { viewModel.trySendAction(ConfirmPasswordInputChange(it)) } }, + showPasswordTestTag = "ConfirmPasswordVisibilityToggle", + cardStyle = CardStyle.Middle(dividerPadding = 0.dp), modifier = Modifier .testTag("ConfirmMasterPasswordEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), - showPasswordTestTag = "ConfirmPasswordVisibilityToggle", + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) BitwardenTextField( label = stringResource(id = R.string.master_password_hint), value = state.passwordHintInput, onValueChange = remember(viewModel) { { viewModel.trySendAction(PasswordHintChange(it)) } }, - hint = stringResource(id = R.string.master_password_hint_description), + supportingText = stringResource(id = R.string.master_password_hint_description), + textFieldTestTag = "MasterPasswordHintLabel", + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "MasterPasswordHintLabel", + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(id = R.string.check_known_data_breaches_for_this_password), isChecked = state.isCheckDataBreachesToggled, @@ -252,11 +262,12 @@ fun CreateAccountScreen( viewModel.trySendAction(CheckDataBreachesToggle(newState = newState)) } }, + cardStyle = CardStyle.Top(), modifier = Modifier .testTag("CheckExposedMasterPasswordToggle") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(8.dp)) TermsAndPrivacySwitch( isChecked = state.isAcceptPoliciesToggled, onCheckedChange = remember(viewModel) { @@ -270,8 +281,9 @@ fun CreateAccountScreen( }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -292,7 +304,7 @@ private fun TermsAndPrivacySwitch( isChecked = isChecked, contentDescription = "AcceptPoliciesToggle", onCheckedChange = onCheckedChange, - subContent = { + supportingTextContent = { FlowRow( horizontalArrangement = Arrangement.Start, modifier = Modifier.fillMaxWidth(), @@ -301,7 +313,7 @@ private fun TermsAndPrivacySwitch( label = stringResource(id = R.string.terms_of_service), onClick = onTermsClick, style = BitwardenTheme.typography.bodyMedium, - innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp), + innerPadding = PaddingValues(vertical = 8.dp, horizontal = 0.dp), ) Text( text = ",", @@ -314,9 +326,10 @@ private fun TermsAndPrivacySwitch( label = stringResource(id = R.string.privacy_policy), onClick = onPrivacyPolicyClick, style = BitwardenTheme.typography.bodyMedium, - innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp), + innerPadding = PaddingValues(vertical = 8.dp, horizontal = 0.dp), ) } }, + cardStyle = CardStyle.Bottom, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt index f489fc94c7..e491cffc52 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -27,11 +27,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager @@ -144,28 +146,31 @@ private fun EnterpriseSignOnScreenContent( .verticalScroll(rememberScrollState()) .fillMaxWidth(), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) Text( text = stringResource(id = R.string.log_in_sso_summary), textAlign = TextAlign.Start, style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), value = state.orgIdentifierInput, onValueChange = onOrgIdentifierInputChange, label = stringResource(id = R.string.org_identifier), textFieldTestTag = "OrgSSOIdentifierEntry", + cardStyle = CardStyle.Full, ) Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt index 7dad832e0a..3508ebd44e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt @@ -29,11 +29,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.BuildConfig import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import kotlinx.collections.immutable.persistentListOf @@ -100,20 +102,22 @@ fun EnvironmentScreen( .imePadding() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.self_hosted_environment), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( label = stringResource(id = R.string.server_url), value = state.serverUrl, placeholder = "ex. https://bitwarden.company.com", - hint = stringResource(id = R.string.self_hosted_environment_footer), + supportingText = stringResource(id = R.string.self_hosted_environment_footer), onValueChange = remember(viewModel) { { viewModel.trySendAction(EnvironmentAction.ServerUrlChange(it)) } }, @@ -127,22 +131,24 @@ fun EnvironmentScreen( persistentListOf() }, keyboardType = KeyboardType.Uri, + textFieldTestTag = "ServerUrlEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ServerUrlEntry", + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.custom_environment), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( label = stringResource(id = R.string.web_vault_url), @@ -151,13 +157,14 @@ fun EnvironmentScreen( { viewModel.trySendAction(EnvironmentAction.WebVaultServerUrlChange(it)) } }, keyboardType = KeyboardType.Uri, + textFieldTestTag = "WebVaultUrlEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "WebVaultUrlEntry", + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( label = stringResource(id = R.string.api_url), @@ -166,13 +173,14 @@ fun EnvironmentScreen( { viewModel.trySendAction(EnvironmentAction.ApiServerUrlChange(it)) } }, keyboardType = KeyboardType.Uri, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("ApiUrlEntry") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( label = stringResource(id = R.string.identity_url), @@ -181,13 +189,14 @@ fun EnvironmentScreen( { viewModel.trySendAction(EnvironmentAction.IdentityServerUrlChange(it)) } }, keyboardType = KeyboardType.Uri, + textFieldTestTag = "IdentityUrlEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityUrlEntry", + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( label = stringResource(id = R.string.icons_url), @@ -195,14 +204,16 @@ fun EnvironmentScreen( onValueChange = remember(viewModel) { { viewModel.trySendAction(EnvironmentAction.IconsServerUrlChange(it)) } }, - hint = stringResource(id = R.string.custom_environment_footer), + supportingText = stringResource(id = R.string.custom_environment_footer), keyboardType = KeyboardType.Uri, + textFieldTestTag = "IconsUrlEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IconsUrlEntry", + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index ca858ace31..15e07035ac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState @@ -39,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.account.BitwardenPlaceholderAccountActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar @@ -48,6 +48,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -216,7 +217,7 @@ private fun LandingScreenContent( colorFilter = ColorFilter.tint(BitwardenTheme.colorScheme.icon.secondary), contentDescription = null, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .width(220.dp) .height(74.dp) .fillMaxWidth(), @@ -232,7 +233,7 @@ private fun LandingScreenContent( style = BitwardenTheme.typography.headlineSmall, color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier - .padding(horizontal = 24.dp) + .standardHorizontalMargin() .wrapContentHeight(), ) @@ -242,38 +243,40 @@ private fun LandingScreenContent( BitwardenTextField( modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), value = state.emailInput, onValueChange = onEmailInputChange, label = stringResource(id = R.string.email_address), keyboardType = KeyboardType.Email, textFieldTestTag = "EmailAddressEntry", + cardStyle = CardStyle.Full, + supportingTextContent = { + EnvironmentSelector( + labelText = stringResource(id = R.string.logging_in_on), + selectedOption = state.selectedEnvironmentType, + onOptionSelected = onEnvironmentTypeSelect, + modifier = Modifier + .testTag("RegionSelectorDropdown") + .fillMaxWidth(), + ) + }, ) - Spacer(modifier = Modifier.height(2.dp)) - - EnvironmentSelector( - labelText = stringResource(id = R.string.logging_in_on), - selectedOption = state.selectedEnvironmentType, - onOptionSelected = onEnvironmentTypeSelect, - modifier = Modifier - .testTag("RegionSelectorDropdown") - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(id = R.string.remember_me), isChecked = state.isRememberMeEnabled, onCheckedChange = onRememberMeToggle, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("RememberMeSwitch") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(height = 24.dp)) BitwardenFilledButton( label = stringResource(id = R.string.continue_text), @@ -281,17 +284,17 @@ private fun LandingScreenContent( isEnabled = state.isContinueButtonEnabled, modifier = Modifier .testTag("ContinueButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 24.dp)) Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth() .wrapContentHeight(), ) { @@ -309,7 +312,7 @@ private fun LandingScreenContent( ) } - Spacer(modifier = Modifier.height(58.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt index d7570e92f3..2e6654068e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -22,6 +21,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -35,6 +35,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.account.BitwardenPlaceholderAccountActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar @@ -45,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -230,28 +232,35 @@ private fun LoginScreenContent( .imePadding() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenPasswordField( autoFocus = true, - modifier = Modifier - .testTag("MasterPasswordEntry") - .padding(horizontal = 16.dp) - .fillMaxWidth(), value = state.passwordInput, onValueChange = onPasswordInputChanged, label = stringResource(id = R.string.master_password), showPasswordTestTag = "PasswordVisibilityToggle", - ) - - BitwardenClickableText( - label = stringResource(id = R.string.get_master_passwordword_hint), - onClick = onMasterPasswordClick, - style = BitwardenTheme.typography.bodySmall, + supportingTextContent = { + BitwardenClickableText( + label = stringResource(id = R.string.get_master_passwordword_hint), + onClick = onMasterPasswordClick, + style = BitwardenTheme.typography.bodySmall, + innerPadding = PaddingValues( + top = 8.dp, + bottom = 8.dp, + start = 0.dp, + end = 16.dp, + ), + modifier = Modifier.testTag("GetMasterPasswordHintLabel"), + ) + }, + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) - .testTag("GetMasterPasswordHintLabel"), + .testTag("MasterPasswordEntry") + .standardHorizontalMargin() + .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(height = 24.dp)) BitwardenFilledButton( label = stringResource(id = R.string.log_in_with_master_password), @@ -259,7 +268,7 @@ private fun LoginScreenContent( isEnabled = state.isLoginButtonEnabled, modifier = Modifier .testTag("LogInWithMasterPasswordButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -272,7 +281,7 @@ private fun LoginScreenContent( onClick = onLoginWithDeviceClick, modifier = Modifier .testTag("LogInWithAnotherDeviceButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -285,7 +294,7 @@ private fun LoginScreenContent( onClick = onSingleSignOnClick, modifier = Modifier .testTag("LogInWithSsoButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -297,12 +306,12 @@ private fun LoginScreenContent( state.emailAddress, state.environmentLabel, ), - textAlign = TextAlign.Start, + textAlign = TextAlign.Center, style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier .testTag("LoggingInAsLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -311,8 +320,12 @@ private fun LoginScreenContent( onClick = onNotYouButtonClick, style = BitwardenTheme.typography.labelLarge, innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), - modifier = Modifier.testTag("NotYouLabel"), + modifier = Modifier + .standardHorizontalMargin() + .align(alignment = Alignment.CenterHorizontally) + .testTag("NotYouLabel"), ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt index 45e2d8f7d9..f615be4ba3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt @@ -33,6 +33,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost @@ -122,6 +123,7 @@ fun MasterPasswordGeneratorScreen( }, modifier = Modifier.standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -137,7 +139,7 @@ private fun MasterPasswordGeneratorContent( Column( modifier = modifier.fillMaxWidth(), ) { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenTextField( label = "", value = generatedPassword, @@ -146,6 +148,7 @@ private fun MasterPasswordGeneratorContent( shouldAddCustomLineBreaks = true, textStyle = BitwardenTheme.typography.sensitiveInfoSmall, visualTransformation = nonLetterColorVisualTransformation(), + cardStyle = CardStyle.Full, modifier = Modifier.fillMaxWidth(), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt index 301347cdeb..2a1912b186 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt @@ -1,11 +1,12 @@ package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -21,14 +22,15 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter -import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** * The top level composable for the Login screen. @@ -107,9 +109,10 @@ fun MasterPasswordHintScreen( Column( modifier = Modifier.fillMaxSize(), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenTextField( modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), value = state.emailInput, onValueChange = remember(viewModel) { @@ -118,19 +121,11 @@ fun MasterPasswordHintScreen( label = stringResource(id = R.string.email_address), keyboardType = KeyboardType.Email, textFieldTestTag = "MasterPasswordHintEmailField", + supportingText = stringResource(id = R.string.enter_email_for_hint), + cardStyle = CardStyle.Full, ) - - Text( - text = stringResource(id = R.string.enter_email_for_hint), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .padding( - vertical = 4.dp, - horizontal = 16.dp, - ), - ) + Spacer(modifier = Modifier.height(height = 16.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt index 3fb2f5cae4..ad4fd58c9f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/newdevicenotice/NewDeviceNoticeEmailAccessScreen.kt @@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -190,15 +191,15 @@ private fun MainContent( ), ), ) - Column { - BitwardenSwitch( - label = stringResource(id = R.string.yes_i_can_reliably_access_my_email), - isChecked = isEmailAccessEnabled, - onCheckedChange = onEmailAccessToggleChanged, - modifier = Modifier - .testTag("EmailAccessToggle"), - ) - } + Spacer(modifier = Modifier.height(height = 12.dp)) + BitwardenSwitch( + label = stringResource(id = R.string.yes_i_can_reliably_access_my_email), + isChecked = isEmailAccessEnabled, + onCheckedChange = onEmailAccessToggleChanged, + cardStyle = CardStyle.Full, + modifier = Modifier + .testTag("EmailAccessToggle"), + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt index 57de5b6201..e13d5f363c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt @@ -31,6 +31,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -87,7 +88,7 @@ private fun RemovePasswordScreenContent( modifier = modifier .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) Text( text = state.description(), @@ -104,11 +105,12 @@ private fun RemovePasswordScreenContent( value = state.input, onValueChange = onInputChanged, showPasswordTestTag = "PasswordVisibilityToggle", + autoFocus = true, + cardStyle = CardStyle.Full, modifier = Modifier .testTag(tag = "MasterPasswordEntry") .standardHorizontalMargin() .fillMaxWidth(), - autoFocus = true, ) Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt index caf7fa4542..5cfdbcb112 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -29,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard @@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold /** @@ -149,7 +150,7 @@ private fun ResetPasswordScreenContent( .imePadding() .verticalScroll(rememberScrollState()), ) { - + Spacer(modifier = Modifier.height(height = 12.dp)) val instructionsTextId = if (state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) { R.string.update_weak_master_password_warning @@ -159,7 +160,7 @@ private fun ResetPasswordScreenContent( BitwardenInfoCalloutCard( text = stringResource(id = instructionsTextId), modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -174,7 +175,7 @@ private fun ResetPasswordScreenContent( BitwardenInfoCalloutCard( text = passwordPolicyContent, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -184,13 +185,12 @@ private fun ResetPasswordScreenContent( label = stringResource(id = R.string.current_master_password), value = state.currentPasswordInput, onValueChange = onCurrentPasswordInputChanged, + cardStyle = CardStyle.Top(dividerPadding = 0.dp), modifier = Modifier .testTag("MasterPasswordField") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - - Spacer(modifier = Modifier.height(16.dp)) } var isPasswordVisible by rememberSaveable { mutableStateOf(false) } @@ -200,37 +200,41 @@ private fun ResetPasswordScreenContent( onValueChange = onPasswordInputChanged, showPassword = isPasswordVisible, showPasswordChange = { isPasswordVisible = it }, + cardStyle = if ( + state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) { + CardStyle.Middle(dividerPadding = 0.dp) + } else { + CardStyle.Top(dividerPadding = 0.dp) + }, modifier = Modifier .testTag("NewPasswordField") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenPasswordField( label = stringResource(id = R.string.retype_master_password), value = state.retypePasswordInput, onValueChange = onRetypePasswordInputChanged, showPassword = isPasswordVisible, showPasswordChange = { isPasswordVisible = it }, + cardStyle = CardStyle.Middle(dividerPadding = 0.dp), modifier = Modifier .testTag("RetypePasswordField") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenTextField( label = stringResource(id = R.string.master_password_hint), value = state.passwordHintInput, onValueChange = onPasswordHintInputChanged, - hint = stringResource(id = R.string.master_password_hint_description), + supportingText = stringResource(id = R.string.master_password_hint_description), + textFieldTestTag = "MasterPasswordHintLabel", + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "MasterPasswordHintLabel", + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.navigationBarsPadding()) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt index 1c50909746..5a9188e1cc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -28,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard @@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -114,6 +115,7 @@ private fun SetPasswordScreenContent( modifier = modifier .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) Text( text = stringResource( id = R.string.your_organization_requires_you_to_set_a_master_password, @@ -121,7 +123,7 @@ private fun SetPasswordScreenContent( style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -130,7 +132,7 @@ private fun SetPasswordScreenContent( BitwardenInfoCalloutCard( text = stringResource(id = R.string.reset_password_auto_enroll_invite_warning), modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -143,40 +145,40 @@ private fun SetPasswordScreenContent( onValueChange = onPasswordInputChanged, showPassword = isPasswordVisible, showPasswordChange = { isPasswordVisible = it }, - hint = stringResource(id = R.string.master_password_description), + supportingText = stringResource(id = R.string.master_password_description), + cardStyle = CardStyle.Top(dividerPadding = 0.dp), modifier = Modifier .testTag("NewPasswordField") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenPasswordField( label = stringResource(id = R.string.retype_master_password), value = state.retypePasswordInput, onValueChange = onRetypePasswordInputChanged, showPassword = isPasswordVisible, showPasswordChange = { isPasswordVisible = it }, + cardStyle = CardStyle.Middle(dividerPadding = 0.dp), modifier = Modifier .testTag("RetypePasswordField") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenTextField( label = stringResource(id = R.string.master_password_hint), value = state.passwordHintInput, onValueChange = onPasswordHintInputChanged, - hint = stringResource(id = R.string.master_password_hint_description), + supportingText = stringResource(id = R.string.master_password_hint_description), + textFieldTestTag = "MasterPasswordHintLabel", + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "MasterPasswordHintLabel", + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 12.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt index d5a7aa20a3..bbf912eade 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt @@ -59,6 +59,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -177,6 +178,7 @@ fun StartRegistrationScreen( isNewOnboardingUiEnabled = state.showNewOnboardingUi, handler = handler, ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -195,7 +197,7 @@ private fun StartRegistrationContent( modifier: Modifier = Modifier, ) { Column(modifier = modifier) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) if (isNewOnboardingUiEnabled) { Image( painter = rememberVectorPainter(id = R.drawable.vault), @@ -210,50 +212,50 @@ private fun StartRegistrationContent( label = stringResource(id = R.string.name), value = nameInput, onValueChange = handler.onNameInputChange, + textFieldTestTag = "NameEntry", + cardStyle = CardStyle.Top(dividerPadding = 0.dp), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), - textFieldTestTag = "NameEntry", ) - Spacer(modifier = Modifier.height(16.dp)) BitwardenTextField( - label = stringResource( - id = R.string.email_address, - ), + label = stringResource(id = R.string.email_address), placeholder = stringResource(R.string.email_address_required), value = emailInput, onValueChange = handler.onEmailInputChange, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin(), keyboardType = KeyboardType.Email, textFieldTestTag = "EmailAddressEntry", - ) - Spacer(modifier = Modifier.height(2.dp)) - Row( + supportingTextContent = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + EnvironmentSelector( + labelText = stringResource(id = R.string.creating_on), + selectedOption = selectedEnvironmentType, + onOptionSelected = handler.onEnvironmentTypeSelect, + modifier = Modifier.testTag("RegionSelectorDropdown"), + ) + if (isNewOnboardingUiEnabled) { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_question_circle_small, + contentDescription = stringResource( + R.string.help_with_server_geolocations, + ), + onClick = handler.onServerGeologyHelpClick, + contentColor = BitwardenTheme.colorScheme.icon.secondary, + // Align with design but keep accessible touch target of IconButton. + modifier = Modifier.offset(x = 16.dp), + ) + } + } + }, + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - EnvironmentSelector( - labelText = stringResource(id = R.string.creating_on), - selectedOption = selectedEnvironmentType, - onOptionSelected = handler.onEnvironmentTypeSelect, - modifier = Modifier - .testTag("RegionSelectorDropdown"), - ) - if (isNewOnboardingUiEnabled) { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_question_circle_small, - contentDescription = stringResource(R.string.help_with_server_geolocations), - onClick = handler.onServerGeologyHelpClick, - contentColor = BitwardenTheme.colorScheme.icon.secondary, - // Align with design but keep accessible touch target of IconButton. - modifier = Modifier.offset(y = (-8f).dp, x = 16.dp), - ) - } - } + ) Spacer(modifier = Modifier.height(24.dp)) if (selectedEnvironmentType != Environment.Type.SELF_HOSTED) { @@ -367,6 +369,7 @@ private fun ReceiveMarketingEmailsSwitch( isChecked = isChecked, onCheckedChange = onCheckedChange, contentDescription = "ReceiveMarketingEmailsToggle", + cardStyle = CardStyle.Full, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt index 72d955d03b..6258d527c2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -18,11 +17,13 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -30,12 +31,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.handlers.TrustedDeviceHandlers import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -118,15 +121,16 @@ private fun TrustedDeviceScaffold( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenSwitch( label = stringResource(id = R.string.remember_this_device), - description = stringResource(id = R.string.turn_off_using_public_device), + supportingText = stringResource(id = R.string.turn_off_using_public_device), isChecked = state.isRemembered, onCheckedChange = handlers.onRememberToggle, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("RememberThisDeviceSwitch") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) Spacer(modifier = Modifier.height(24.dp)) @@ -136,7 +140,7 @@ private fun TrustedDeviceScaffold( label = stringResource(id = R.string.continue_text), onClick = handlers.onContinueClick, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) Spacer(modifier = Modifier.height(12.dp)) @@ -148,7 +152,7 @@ private fun TrustedDeviceScaffold( onClick = handlers.onApproveWithDeviceClick, modifier = Modifier .testTag("ApproveWithOtherDeviceButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) Spacer(modifier = Modifier.height(12.dp)) @@ -159,7 +163,7 @@ private fun TrustedDeviceScaffold( label = stringResource(id = R.string.request_admin_approval), onClick = handlers.onApproveWithAdminClick, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth() .testTag("RequestAdminApprovalButton"), ) @@ -172,7 +176,7 @@ private fun TrustedDeviceScaffold( onClick = handlers.onApproveWithPasswordClick, modifier = Modifier .testTag("ApproveWithMasterPasswordButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) Spacer(modifier = Modifier.height(12.dp)) @@ -187,9 +191,10 @@ private fun TrustedDeviceScaffold( ), style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.secondary, + textAlign = TextAlign.Center, modifier = Modifier .testTag("LoggingInAsLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -198,9 +203,12 @@ private fun TrustedDeviceScaffold( onClick = handlers.onNotYouButtonClick, style = BitwardenTheme.typography.labelLarge, innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), - modifier = Modifier.testTag("NotYouLabel"), + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .testTag("NotYouLabel"), ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt index 7e96eec8e7..c5992abeab 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -42,6 +43,7 @@ import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.description import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.title import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData @@ -50,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -217,13 +220,14 @@ private fun TwoFactorLoginScreenContent( .imePadding() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) Text( text = state.authMethod.description(state.displayEmail)(), textAlign = TextAlign.Start, style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -255,31 +259,33 @@ private fun TwoFactorLoginScreenContent( keyboardActions = KeyboardActions( onDone = { onContinueButtonClick() }, ), + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) } BitwardenSwitch( label = stringResource(id = R.string.remember_me), isChecked = state.isRememberMeEnabled, onCheckedChange = onRememberMeToggle, + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(height = 24.dp)) BitwardenFilledButton( label = state.buttonText(), onClick = onContinueButtonClick, isEnabled = state.isContinueButtonEnabled, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -290,10 +296,12 @@ private fun TwoFactorLoginScreenContent( label = stringResource(id = R.string.send_verification_code_again), onClick = onResendEmailButtonClick, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) } + Spacer(modifier = Modifier.height(height = 16.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -303,7 +311,7 @@ private fun TwoFactorLoginScreenContentPreview() { BitwardenTheme { TwoFactorLoginScreenContent( state = TwoFactorLoginState( - TwoFactorAuthMethod.EMAIL, + authMethod = TwoFactorAuthMethod.EMAIL, availableAuthMethods = listOf(TwoFactorAuthMethod.EMAIL), codeInput = "", dialogState = null, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index 539e55c7b6..04829d1545 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.verticalScroll @@ -47,6 +46,9 @@ import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenMessage import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenTitle import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountActionItem import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar @@ -58,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLogoutConfirmationDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager @@ -247,6 +250,7 @@ fun VaultUnlockScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(12.dp)) if (!state.hideInput) { BitwardenPasswordField( label = state.vaultUnlockType.unlockScreenInputLabel(), @@ -258,10 +262,6 @@ fun VaultUnlockScreen( showPasswordTestTag = state .vaultUnlockType .inputFieldVisibilityToggleTestTag, - modifier = Modifier - .testTag(state.vaultUnlockType.unlockScreenInputTestTag) - .padding(horizontal = 16.dp) - .fillMaxWidth(), autoFocus = state.showKeyboard, imeAction = ImeAction.Done, keyboardActions = KeyboardActions( @@ -269,17 +269,18 @@ fun VaultUnlockScreen( { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } }, ), - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = state.vaultUnlockType.unlockScreenMessage(), - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.primary, + supportingText = state.vaultUnlockType.unlockScreenMessage(), + cardStyle = CardStyle.Top(hasDivider = false), modifier = Modifier - .padding(horizontal = 16.dp) + .testTag(tag = state.vaultUnlockType.unlockScreenInputTestTag) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) + } + val trailingTextCardStyle = if (state.hideInput) { + CardStyle.Full + } else { + CardStyle.Bottom } Text( text = stringResource( @@ -287,11 +288,19 @@ fun VaultUnlockScreen( state.email, state.environmentUrl, ), - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, modifier = Modifier .testTag("UserAndEnvironmentDataLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() + .cardBackground(cardStyle = trailingTextCardStyle) + .cardPadding( + cardStyle = trailingTextCardStyle, + start = 16.dp, + end = 16.dp, + top = 0.dp, + bottom = 12.dp, + ) .fillMaxWidth(), ) Spacer(modifier = Modifier.height(24.dp)) @@ -302,7 +311,7 @@ fun VaultUnlockScreen( { viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) } }, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) Spacer(modifier = Modifier.height(12.dp)) @@ -312,7 +321,7 @@ fun VaultUnlockScreen( textAlign = TextAlign.Start, style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.status.error, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(12.dp)) } @@ -325,7 +334,7 @@ fun VaultUnlockScreen( isEnabled = state.input.isNotEmpty(), modifier = Modifier .testTag("UnlockVaultButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ListExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ListExtensions.kt index 33d0722e9a..05f3b242b6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ListExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ListExtensions.kt @@ -1,5 +1,9 @@ package com.x8bit.bitwarden.ui.platform.base.util +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle + /** * Returns null if all entries in a given list are equal to a provided [value], otherwise * the original list is returned. @@ -10,3 +14,21 @@ fun List.nullIfAllEqual(value: T): List? = } else { this } + +/** + * Returns the appropriate [CardStyle] based on the current [index] in the list. + */ +fun Collection.toListItemCardStyle( + index: Int, + hasDivider: Boolean = true, + dividerPadding: Dp = 16.dp, +): CardStyle = + if (this.size == 1) { + CardStyle.Full + } else if (index == 0) { + CardStyle.Top(hasDivider = hasDivider, dividerPadding = dividerPadding) + } else if (index == this.size - 1) { + CardStyle.Bottom + } else { + CardStyle.Middle(hasDivider = hasDivider, dividerPadding = dividerPadding) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt index 5f0432196e..ab841b9a80 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt @@ -1,5 +1,7 @@ package com.x8bit.bitwarden.ui.platform.base.util +import android.os.Build +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarScrollBehavior @@ -7,12 +9,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.CombinedModifier import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.isShiftPressed @@ -26,6 +30,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.isPortrait @@ -159,3 +164,72 @@ fun Modifier.standardHorizontalMargin( val config = LocalConfiguration.current return this.padding(horizontal = if (config.isPortrait) portrait else landscape) } + +/** + * This is a [Modifier] extension that applies a card background to the content. + */ +@OmitFromCoverage +@Stable +@Composable +fun Modifier.cardBackground( + cardStyle: CardStyle?, + color: Color = BitwardenTheme.colorScheme.background.secondary, +): Modifier { + cardStyle ?: return this + val shape = if ("robolectric" == Build.FINGERPRINT) { + // TODO: This is here to ensure our click events work in robolectric tests because of the + // uneven rounded corners that need to be clipped. This should be removed when the bug is + // resolved: https://issuetracker.google.com/issues/366255137 + RectangleShape + } else { + when (cardStyle) { + is CardStyle.Top -> BitwardenTheme.shapes.contentTop + is CardStyle.Middle -> BitwardenTheme.shapes.contentMiddle + CardStyle.Bottom -> BitwardenTheme.shapes.contentBottom + CardStyle.Full -> BitwardenTheme.shapes.content + } + } + return this + .clip(shape = shape) + .background(color = color, shape = shape) + .bottomDivider( + paddingStart = cardStyle.dividerPadding, + enabled = cardStyle.hasDivider, + ) +} + +/** + * This is a [Modifier] extension that applies card padding to the content. + */ +@OmitFromCoverage +@Stable +@Composable +fun Modifier.cardPadding( + cardStyle: CardStyle?, + start: Dp = 0.dp, + top: Dp = 12.dp, + end: Dp = 0.dp, + bottom: Dp = 12.dp, +): Modifier { + cardStyle ?: return this + return this.padding(start = start, top = top, end = end, bottom = bottom) +} + +/** + * This is a [Modifier] extension that applies card padding to the content. + */ +@OmitFromCoverage +@Stable +@Composable +fun Modifier.cardPadding( + cardStyle: CardStyle?, + vertical: Dp = 12.dp, + horizontal: Dp = 0.dp, +): Modifier = + this.cardPadding( + cardStyle = cardStyle, + start = horizontal, + top = vertical, + end = horizontal, + bottom = vertical, + ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt index 2300591e3a..589286e14a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -74,10 +73,7 @@ fun BitwardenSearchTopAppBar( }, title = { TextField( - colors = bitwardenTextFieldColors( - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - ), + colors = bitwardenTextFieldColors(), textStyle = BitwardenTheme.typography.bodyLarge, placeholder = { Text(text = placeholder) }, value = searchTerm, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt index 1f8564e9dc..376f255e06 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import com.x8bit.bitwarden.R @@ -30,7 +31,9 @@ fun BitwardenFilledIconButton( isEnabled: Boolean = true, ) { FilledIconButton( - modifier = modifier.semantics(mergeDescendants = true) {}, + modifier = modifier.semantics(mergeDescendants = true) { + this.contentDescription = contentDescription + }, onClick = onClick, colors = bitwardenFilledIconButtonColors(), enabled = isEnabled, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenStandardIconButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenStandardIconButton.kt index 6284302a18..04842351dd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenStandardIconButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenStandardIconButton.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import com.x8bit.bitwarden.R @@ -61,7 +62,9 @@ fun BitwardenStandardIconButton( contentColor: Color = BitwardenTheme.colorScheme.icon.primary, ) { IconButton( - modifier = modifier.semantics(mergeDescendants = true) {}, + modifier = modifier.semantics(mergeDescendants = true) { + this.contentDescription = contentDescription + }, onClick = onClick, colors = bitwardenStandardIconButtonColors(contentColor = contentColor), enabled = isEnabled, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTonalIconButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTonalIconButton.kt index 49418d2d97..64ee6e9199 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTonalIconButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenTonalIconButton.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import com.x8bit.bitwarden.R @@ -30,7 +31,9 @@ fun BitwardenTonalIconButton( isEnabled: Boolean = true, ) { FilledIconButton( - modifier = modifier.semantics(mergeDescendants = true) {}, + modifier = modifier.semantics(mergeDescendants = true) { + this.contentDescription = contentDescription + }, onClick = onClick, colors = bitwardenTonalIconButtonColors(), enabled = isEnabled, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt index 5ec838818c..85cea5fd00 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt @@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.components.dialog import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerColors import androidx.compose.material3.DatePickerDialog @@ -9,7 +11,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,10 +31,14 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.orNow @@ -51,6 +59,7 @@ import java.time.ZonedDateTime * @param onDateSelect The callback to be invoked when a new date is selected. * @param isEnabled Whether the button is enabled. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param cardStyle Indicates the type of card style to be applied. */ @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @@ -62,6 +71,7 @@ fun BitwardenDateSelectButton( onDateSelect: (ZonedDateTime) -> Unit, isEnabled: Boolean, modifier: Modifier = Modifier, + cardStyle: CardStyle? = null, ) { var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) } val formattedDate by remember(currentZonedDateTime) { @@ -72,18 +82,22 @@ fun BitwardenDateSelectButton( ) } - OutlinedTextField( + TextField( modifier = modifier .clearAndSetSemantics { role = Role.DropdownList contentDescription = "$label, $formattedDate" } + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( enabled = isEnabled, - indication = null, + indication = ripple(color = BitwardenTheme.colorScheme.background.pressed), interactionSource = remember { MutableInteractionSource() }, onClick = { shouldShowDialog = !shouldShowDialog }, - ), + ) + .cardPadding(cardStyle = cardStyle) + .padding(top = 4.dp), textStyle = BitwardenTheme.typography.bodyLarge, readOnly = true, label = { Text(text = label) }, @@ -173,5 +187,9 @@ private fun bitwardenDatePickerColors(): DatePickerColors = DatePickerColors( dayInSelectionRangeContainerColor = BitwardenTheme.colorScheme.filledButton.background, dividerColor = BitwardenTheme.colorScheme.stroke.divider, dayInSelectionRangeContentColor = BitwardenTheme.colorScheme.text.primary, - dateTextFieldColors = bitwardenTextFieldColors(), + dateTextFieldColors = bitwardenTextFieldColors( + disabledBorderColor = BitwardenTheme.colorScheme.outlineButton.borderDisabled, + focusedBorderColor = BitwardenTheme.colorScheme.stroke.border, + unfocusedBorderColor = BitwardenTheme.colorScheme.stroke.divider, + ), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenMasterPasswordDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenMasterPasswordDialog.kt index feb22d5d31..ee90a928dd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenMasterPasswordDialog.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenMasterPasswordDialog.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -76,10 +77,11 @@ fun BitwardenMasterPasswordDialog( label = stringResource(id = R.string.master_password), value = masterPassword, onValueChange = { masterPassword = it }, + autoFocus = true, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("AlertInputField") .imePadding(), - autoFocus = true, ) } }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenPinDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenPinDialog.kt index a4098bfad9..f4add44a11 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenPinDialog.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenPinDialog.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.components.dialog +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text @@ -18,6 +19,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -63,10 +65,12 @@ fun BitwardenPinDialog( label = stringResource(id = R.string.pin), value = pin, onValueChange = { pin = it }, + autoFocus = true, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("AlertInputField") + .fillMaxWidth() .imePadding(), - autoFocus = true, ) }, shape = BitwardenTheme.shapes.dialog, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTextEntryDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTextEntryDialog.kt index 49edf0df06..471af40498 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTextEntryDialog.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTextEntryDialog.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.components.dialog +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,6 +22,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -77,12 +79,12 @@ fun BitwardenTextEntryDialog( label = textFieldLabel, value = text, onValueChange = { text = it }, + textFieldTestTag = "AlertContentText", + cardStyle = CardStyle.Full, modifier = Modifier .focusRequester(focusRequester) - .onGloballyPositioned { - shouldRequestFocus = true - }, - textFieldTestTag = "AlertContentText", + .onGloballyPositioned { shouldRequestFocus = true } + .fillMaxWidth(), ) }, shape = BitwardenTheme.shapes.dialog, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt index 897de9c057..3030def5e6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt @@ -2,9 +2,13 @@ package com.x8bit.bitwarden.ui.platform.components.dialog import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,8 +20,12 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.orNow @@ -36,6 +44,7 @@ import java.time.ZonedDateTime * @param onTimeSelect The callback to be invoked when a new time is selected. * @param isEnabled Whether the button is enabled. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param cardStyle Indicates the type of card style to be applied. * @param is24Hour Indicates if the time selector should use a 24 hour format or a 12 hour format * with AM/PM. */ @@ -47,6 +56,7 @@ fun BitwardenTimeSelectButton( onTimeSelect: (hour: Int, minute: Int) -> Unit, isEnabled: Boolean, modifier: Modifier = Modifier, + cardStyle: CardStyle? = null, is24Hour: Boolean = false, ) { var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) } @@ -57,18 +67,22 @@ fun BitwardenTimeSelectButton( ?: "--:-- --", ) } - OutlinedTextField( + TextField( modifier = modifier .clearAndSetSemantics { role = Role.DropdownList contentDescription = "$label, $formattedTime" } + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( enabled = isEnabled, - indication = null, + indication = ripple(color = BitwardenTheme.colorScheme.background.pressed), interactionSource = remember { MutableInteractionSource() }, onClick = { shouldShowDialog = !shouldShowDialog }, - ), + ) + .cardPadding(cardStyle = cardStyle) + .padding(top = 4.dp), textStyle = BitwardenTheme.typography.bodyLarge, readOnly = true, label = { Text(text = label) }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt index d071959e1e..7f28f0332c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt @@ -2,14 +2,20 @@ package com.x8bit.bitwarden.ui.platform.components.dropdown import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer 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.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -17,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.Role @@ -29,11 +36,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.TooltipData +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import kotlinx.collections.immutable.ImmutableList @@ -55,6 +67,12 @@ import kotlinx.collections.immutable.persistentListOf * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. * @param supportingText A optional supporting text that will appear below the text field. * @param tooltip A nullable [TooltipData], representing the tooltip icon. + * @param insets Inner padding to be applied withing the card. + * @param cardStyle Indicates the type of card style to be applied. + * @param actionsPadding Padding to be applied to the [actions] block. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. */ @Suppress("LongMethod") @Composable @@ -67,10 +85,14 @@ fun BitwardenMultiSelectButton( isEnabled: Boolean = true, supportingText: String? = null, tooltip: TooltipData? = null, + insets: PaddingValues = PaddingValues(), + cardStyle: CardStyle? = null, + actionsPadding: PaddingValues = PaddingValues(), + actions: @Composable RowScope.() -> Unit = {}, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(false) } - OutlinedTextField( + Column( modifier = modifier .clearAndSetSemantics { role = Role.DropdownList @@ -89,7 +111,7 @@ fun BitwardenMultiSelectButton( }, ) } - .fillMaxWidth() + .cardBackground(cardStyle = cardStyle) .clickable( indication = ripple( color = BitwardenTheme.colorScheme.background.pressed, @@ -98,48 +120,74 @@ fun BitwardenMultiSelectButton( interactionSource = remember { MutableInteractionSource() }, ) { shouldShowDialog = !shouldShowDialog - }, - textStyle = BitwardenTheme.typography.bodyLarge, - readOnly = true, - label = { - Row { - Text( - text = label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - tooltip?.let { - Spacer(modifier = Modifier.width(3.dp)) - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_question_circle_small, - contentDescription = it.contentDescription, - onClick = it.onClick, - isEnabled = isEnabled, - contentColor = BitwardenTheme.colorScheme.icon.secondary, - modifier = Modifier.size(16.dp), + } + .cardPadding(cardStyle = cardStyle, vertical = 6.dp) + .padding(paddingValues = insets), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + TextField( + textStyle = BitwardenTheme.typography.bodyLarge, + readOnly = true, + label = { + Row { + Text( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + tooltip?.let { + Spacer(modifier = Modifier.width(3.dp)) + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_question_circle_small, + contentDescription = it.contentDescription, + onClick = it.onClick, + isEnabled = isEnabled, + contentColor = BitwardenTheme.colorScheme.icon.secondary, + modifier = Modifier.size(16.dp), + ) + } + } + }, + value = selectedOption.orEmpty(), + onValueChange = onOptionSelected, + enabled = shouldShowDialog, + trailingIcon = { + Icon( + painter = rememberVectorPainter(id = R.drawable.ic_chevron_down), + contentDescription = null, ) - } - } - }, - value = selectedOption.orEmpty(), - onValueChange = onOptionSelected, - enabled = shouldShowDialog, - trailingIcon = { - Icon( - painter = rememberVectorPainter(id = R.drawable.ic_chevron_down), - contentDescription = null, + }, + colors = bitwardenTextFieldButtonColors(), + modifier = Modifier + .weight(weight = 1f) + .fillMaxWidth(), ) - }, - colors = bitwardenTextFieldButtonColors(), - supportingText = supportingText?.let { - { - Text( - text = supportingText, - style = BitwardenTheme.typography.bodySmall, - ) - } - }, - ) + BitwardenRowOfActions( + modifier = Modifier.padding(paddingValues = actionsPadding), + actions = actions, + ) + } + supportingText?.let { + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 8.dp)) + } + } if (shouldShowDialog) { BitwardenSelectionDialog( title = label, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/EnvironmentSelector.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/EnvironmentSelector.kt index ba95962b3f..e0b28e7432 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/EnvironmentSelector.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/EnvironmentSelector.kt @@ -64,10 +64,7 @@ fun EnvironmentSelector( interactionSource = remember { MutableInteractionSource() }, onClick = { shouldShowDialog = !shouldShowDialog }, ) - .padding( - vertical = 8.dp, - horizontal = 16.dp, - ), + .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenHiddenPasswordField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenHiddenPasswordField.kt index 644a19aa0b..8d7497424d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenHiddenPasswordField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenHiddenPasswordField.kt @@ -1,8 +1,8 @@ package com.x8bit.bitwarden.ui.platform.components.field import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier @@ -10,8 +10,10 @@ import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -20,16 +22,18 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param label Label for the text field. * @param value Current text on the text field. * @param modifier Modifier for the composable. + * @param cardStyle Indicates the type of card style to be applied. */ @Composable fun BitwardenHiddenPasswordField( label: String, value: String, modifier: Modifier = Modifier, + cardStyle: CardStyle? = null, ) { CompositionLocalProvider(value = LocalTextToolbar provides BitwardenEmptyTextToolbar) { - OutlinedTextField( - modifier = modifier, + TextField( + modifier = modifier.cardBackground(cardStyle = cardStyle), textStyle = BitwardenTheme.typography.sensitiveInfoSmall, label = { Text(text = label) }, value = value, @@ -51,6 +55,7 @@ private fun BitwardenHiddenPasswordField_preview() { BitwardenHiddenPasswordField( label = "Label", value = "Password", + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt index 9cea17222a..0be8b515fd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt @@ -1,9 +1,16 @@ package com.x8bit.bitwarden.ui.platform.components.field +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -28,12 +35,17 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.tabNavigation import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -48,11 +60,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param showPassword Whether or not password should be shown. * @param showPasswordChange Lambda that is called when user request show/hide be toggled. * @param onValueChange Callback that is triggered when the password changes. + * @param supportingTextContent An optional supporting text composable will appear below the text + * input. * @param modifier Modifier for the composable. * @param readOnly `true` if the input should be read-only and not accept user interactions. * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. - * @param hint optional hint text that will appear below the text input. * @param showPasswordTestTag The test tag to be used on the show password button (testing tool). * @param autoFocus When set to true, the view will request focus after the first recomposition. * Setting this to true on multiple fields at once may have unexpected consequences. @@ -61,6 +74,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param imeAction the preferred IME action for the keyboard to have. * @param keyboardActions the callbacks of keyboard actions. * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. */ @Suppress("LongMethod") @Composable @@ -70,16 +84,17 @@ fun BitwardenPasswordField( showPassword: Boolean, showPasswordChange: (Boolean) -> Unit, onValueChange: (String) -> Unit, + supportingTextContent: @Composable (ColumnScope.() -> Unit)?, modifier: Modifier = Modifier, readOnly: Boolean = false, singleLine: Boolean = true, - hint: String? = null, showPasswordTestTag: String? = null, autoFocus: Boolean = false, keyboardType: KeyboardType = KeyboardType.Password, imeAction: ImeAction = ImeAction.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + cardStyle: CardStyle? = null, ) { val focusRequester = remember { FocusRequester() } var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) } @@ -103,63 +118,214 @@ fun BitwardenPasswordField( } var lastTextValue by remember(value) { mutableStateOf(value = value) } CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) { - OutlinedTextField( + Column( modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, vertical = 6.dp) .tabNavigation() - .focusRequester(focusRequester), - colors = bitwardenTextFieldColors(), - textStyle = BitwardenTheme.typography.sensitiveInfoSmall, - label = { Text(text = label) }, - value = textFieldValue, - onValueChange = { - textFieldValueState = it - val stringChangedSinceLastInvocation = lastTextValue != it.text - lastTextValue = it.text - if (stringChangedSinceLastInvocation) { - onValueChange(it.text) - } - }, - visualTransformation = when { - !showPassword -> PasswordVisualTransformation() - readOnly -> nonLetterColorVisualTransformation() - else -> VisualTransformation.None - }, - singleLine = singleLine, - readOnly = readOnly, - keyboardOptions = KeyboardOptions( - keyboardType = keyboardType, - imeAction = imeAction, - ), - keyboardActions = keyboardActions, - supportingText = hint?.let { - { - Text( - text = hint, - style = BitwardenTheme.typography.bodySmall, + .focusRequester(focusRequester = focusRequester), + ) { + TextField( + colors = bitwardenTextFieldColors(), + textStyle = BitwardenTheme.typography.sensitiveInfoSmall, + label = { Text(text = label) }, + value = textFieldValue, + onValueChange = { + textFieldValueState = it + val stringChangedSinceLastInvocation = lastTextValue != it.text + lastTextValue = it.text + if (stringChangedSinceLastInvocation) { + onValueChange(it.text) + } + }, + visualTransformation = when { + !showPassword -> PasswordVisualTransformation() + readOnly -> nonLetterColorVisualTransformation() + else -> VisualTransformation.None + }, + singleLine = singleLine, + readOnly = readOnly, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + imeAction = imeAction, + ), + keyboardActions = keyboardActions, + trailingIcon = { + BitwardenStandardIconButton( + modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } }, + vectorIconRes = if (showPassword) { + R.drawable.ic_eye_slash + } else { + R.drawable.ic_eye + }, + contentDescription = stringResource( + id = if (showPassword) R.string.hide else R.string.show, + ), + onClick = { showPasswordChange.invoke(!showPassword) }, ) - } - }, - trailingIcon = { - BitwardenStandardIconButton( - modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } }, - vectorIconRes = if (showPassword) { - R.drawable.ic_eye_slash - } else { - R.drawable.ic_eye - }, - contentDescription = stringResource( - id = if (showPassword) R.string.hide else R.string.show, - ), - onClick = { showPasswordChange.invoke(!showPassword) }, + }, + modifier = Modifier.fillMaxWidth(), + ) + supportingTextContent?.let { + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), ) - }, - ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Column( + modifier = Modifier.padding(horizontal = 16.dp), + content = it, + ) + Spacer(modifier = Modifier.height(height = 6.dp)) + } + } } if (autoFocus) { LaunchedEffect(Unit) { focusRequester.requestFocus() } } } +/** + * Represents a Bitwarden-styled password field that hoists show/hide password state to the caller. + * + * See overloaded [BitwardenPasswordField] for self managed show/hide state. + * + * @param label Label for the text field. + * @param value Current next on the text field. + * @param showPassword Whether or not password should be shown. + * @param showPasswordChange Lambda that is called when user request show/hide be toggled. + * @param onValueChange Callback that is triggered when the password changes. + * @param modifier Modifier for the composable. + * @param readOnly `true` if the input should be read-only and not accept user interactions. + * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls + * instead of wrapping onto multiple lines. + * @param supportingText An optional supporting text that will appear below the text input. + * @param showPasswordTestTag The test tag to be used on the show password button (testing tool). + * @param autoFocus When set to true, the view will request focus after the first recomposition. + * Setting this to true on multiple fields at once may have unexpected consequences. + * @param keyboardType The type of keyboard the user has access to when inputting values into + * the password field. + * @param imeAction the preferred IME action for the keyboard to have. + * @param keyboardActions the callbacks of keyboard actions. + * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. + */ +@Composable +fun BitwardenPasswordField( + label: String, + value: String, + showPassword: Boolean, + showPasswordChange: (Boolean) -> Unit, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + readOnly: Boolean = false, + singleLine: Boolean = true, + supportingText: String? = null, + showPasswordTestTag: String? = null, + autoFocus: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Password, + imeAction: ImeAction = ImeAction.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + cardStyle: CardStyle? = null, +) { + BitwardenPasswordField( + label = label, + value = value, + showPassword = showPassword, + showPasswordChange = showPasswordChange, + showPasswordTestTag = showPasswordTestTag, + onValueChange = onValueChange, + modifier = modifier, + readOnly = readOnly, + singleLine = singleLine, + supportingTextContent = supportingText?.let { + { + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + autoFocus = autoFocus, + keyboardType = keyboardType, + imeAction = imeAction, + keyboardActions = keyboardActions, + textToolbarType = textToolbarType, + cardStyle = cardStyle, + ) +} + +/** + * Represents a Bitwarden-styled password field that hoists show/hide password state to the caller. + * + * See overloaded [BitwardenPasswordField] for self managed show/hide state. + * + * @param label Label for the text field. + * @param value Current next on the text field. + * @param onValueChange Callback that is triggered when the password changes. + * @param modifier Modifier for the composable. + * @param initialShowPassword The initial state of the show/hide password control. A value of + * `false` (the default) indicates that that password should begin in the hidden state. + * @param readOnly `true` if the input should be read-only and not accept user interactions. + * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls + * instead of wrapping onto multiple lines. + * @param supportingTextContent An optional supporting text composable will appear below the text + * input. + * @param showPasswordTestTag The test tag to be used on the show password button (testing tool). + * @param autoFocus When set to true, the view will request focus after the first recomposition. + * Setting this to true on multiple fields at once may have unexpected consequences. + * @param keyboardType The type of keyboard the user has access to when inputting values into + * the password field. + * @param imeAction the preferred IME action for the keyboard to have. + * @param keyboardActions the callbacks of keyboard actions. + * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. + */ +@Composable +fun BitwardenPasswordField( + label: String, + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + initialShowPassword: Boolean = false, + readOnly: Boolean = false, + singleLine: Boolean = true, + supportingTextContent: @Composable (ColumnScope.() -> Unit)?, + showPasswordTestTag: String? = null, + autoFocus: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Password, + imeAction: ImeAction = ImeAction.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + cardStyle: CardStyle? = null, +) { + var showPassword by rememberSaveable { mutableStateOf(value = initialShowPassword) } + BitwardenPasswordField( + label = label, + value = value, + showPassword = showPassword, + showPasswordChange = { showPassword = !showPassword }, + showPasswordTestTag = showPasswordTestTag, + onValueChange = onValueChange, + modifier = modifier, + readOnly = readOnly, + singleLine = singleLine, + supportingTextContent = supportingTextContent, + autoFocus = autoFocus, + keyboardType = keyboardType, + imeAction = imeAction, + keyboardActions = keyboardActions, + textToolbarType = textToolbarType, + cardStyle = cardStyle, + ) +} + /** * Represents a Bitwarden-styled password field that manages the state of a show/hide indicator * internally. @@ -171,7 +337,7 @@ fun BitwardenPasswordField( * @param readOnly `true` if the input should be read-only and not accept user interactions. * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. - * @param hint optional hint text that will appear below the text input. + * @param supportingText An optional supporting text that will appear below the text input. * @param initialShowPassword The initial state of the show/hide password control. A value of * `false` (the default) indicates that that password should begin in the hidden state. * @param showPasswordTestTag The test tag to be used on the show password button (testing tool). @@ -181,6 +347,7 @@ fun BitwardenPasswordField( * the password field. * @param imeAction the preferred IME action for the keyboard to have. * @param keyboardActions the callbacks of keyboard actions. + * @param cardStyle Indicates the type of card style to be applied. */ @Composable fun BitwardenPasswordField( @@ -190,13 +357,14 @@ fun BitwardenPasswordField( modifier: Modifier = Modifier, readOnly: Boolean = false, singleLine: Boolean = true, - hint: String? = null, + supportingText: String? = null, initialShowPassword: Boolean = false, showPasswordTestTag: String? = null, autoFocus: Boolean = false, keyboardType: KeyboardType = KeyboardType.Password, imeAction: ImeAction = ImeAction.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, + cardStyle: CardStyle? = null, ) { var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) } BitwardenPasswordField( @@ -208,59 +376,50 @@ fun BitwardenPasswordField( onValueChange = onValueChange, readOnly = readOnly, singleLine = singleLine, - hint = hint, + supportingText = supportingText, showPasswordTestTag = showPasswordTestTag, autoFocus = autoFocus, keyboardType = keyboardType, imeAction = imeAction, keyboardActions = keyboardActions, + cardStyle = cardStyle, ) } @Preview @Composable -private fun BitwardenPasswordField_preview_withInput_hidePassword() { - BitwardenPasswordField( - label = "Label", - value = "Password", - onValueChange = {}, - initialShowPassword = false, - hint = "Hint", - ) -} - -@Preview -@Composable -private fun BitwardenPasswordField_preview_withInput_showPassword() { - BitwardenPasswordField( - label = "Label", - value = "Password", - onValueChange = {}, - initialShowPassword = true, - hint = "Hint", - ) -} - -@Preview -@Composable -private fun BitwardenPasswordField_preview_withoutInput_hidePassword() { - BitwardenPasswordField( - label = "Label", - value = "", - onValueChange = {}, - initialShowPassword = false, - hint = "Hint", - ) -} - -@Preview -@Composable -private fun BitwardenPasswordField_preview_withoutInput_showPassword() { - BitwardenPasswordField( - label = "Label", - value = "", - onValueChange = {}, - initialShowPassword = true, - hint = "Hint", - ) +private fun BitwardenPasswordField_preview() { + BitwardenTheme { + Column { + BitwardenPasswordField( + label = "Label", + value = "Password", + onValueChange = {}, + initialShowPassword = false, + cardStyle = CardStyle.Top(), + ) + BitwardenPasswordField( + label = "Label", + value = "Password", + onValueChange = {}, + initialShowPassword = true, + cardStyle = CardStyle.Middle(), + ) + BitwardenPasswordField( + label = "Label", + value = "", + onValueChange = {}, + initialShowPassword = false, + cardStyle = CardStyle.Middle(), + ) + BitwardenPasswordField( + label = "Label", + value = "", + onValueChange = {}, + initialShowPassword = true, + supportingText = "Hint", + cardStyle = CardStyle.Bottom, + ) + } + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordFieldWithActions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordFieldWithActions.kt index 543d5ca430..32fa99bea1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordFieldWithActions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenPasswordFieldWithActions.kt @@ -1,8 +1,16 @@ package com.x8bit.bitwarden.ui.platform.components.field +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,8 +24,13 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -32,10 +45,14 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param showPasswordChange Lambda that is called when user request show/hide be toggled. * @param onValueChange Callback that is triggered when the password changes. * @param modifier Modifier for the composable. + * @param supportingText An optional supporting text that will appear below the text field and + * actions. * @param readOnly `true` if the input should be read-only and not accept user interactions. * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. + * @param actionsPadding Padding to be applied to the [actions] block. * @param actions A lambda containing the set of actions (usually icons or similar) to display * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. @@ -48,33 +65,62 @@ fun BitwardenPasswordFieldWithActions( showPasswordChange: (Boolean) -> Unit, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + supportingText: String? = null, readOnly: Boolean = false, singleLine: Boolean = false, showPasswordTestTag: String? = null, passwordFieldTestTag: String? = null, textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + cardStyle: CardStyle? = null, + actionsPadding: PaddingValues = PaddingValues(end = 4.dp), actions: @Composable RowScope.() -> Unit = {}, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.semantics(mergeDescendants = true) {}, + Column( + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, vertical = 6.dp) + .semantics(mergeDescendants = true) { }, + verticalArrangement = Arrangement.Center, ) { - BitwardenPasswordField( - label = label, - value = value, - showPasswordChange = showPasswordChange, - showPassword = showPassword, - onValueChange = onValueChange, - readOnly = readOnly, - singleLine = singleLine, - modifier = Modifier - .semantics { passwordFieldTestTag?.let { testTag = it } } - .weight(1f) - .padding(end = 8.dp), - showPasswordTestTag = showPasswordTestTag, - textToolbarType = textToolbarType, - ) - actions() + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + BitwardenPasswordField( + label = label, + value = value, + showPasswordChange = showPasswordChange, + showPassword = showPassword, + onValueChange = onValueChange, + readOnly = readOnly, + singleLine = singleLine, + showPasswordTestTag = showPasswordTestTag, + textToolbarType = textToolbarType, + modifier = Modifier + .semantics { passwordFieldTestTag?.let { testTag = it } } + .weight(weight = 1f), + ) + BitwardenRowOfActions( + modifier = Modifier.padding(paddingValues = actionsPadding), + actions = actions, + ) + } + supportingText?.let { + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + } } } @@ -94,6 +140,11 @@ fun BitwardenPasswordFieldWithActions( * @param initialShowPassword The initial state of the show/hide password control. A value of * `false` (the default) indicates that that password should begin in the hidden state. * @param actions A lambda containing the set of actions (usually icons or similar) to display + * @param cardStyle Indicates the type of card style to be applied. + * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + * @param actionsPadding Padding to be applied to the [actions] block. + * @param actions A lambda containing the set of actions (usually icons or similar) to display * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. */ @@ -108,6 +159,8 @@ fun BitwardenPasswordFieldWithActions( initialShowPassword: Boolean = false, showPasswordTestTag: String? = null, passwordFieldTestTag: String? = null, + cardStyle: CardStyle? = null, + actionsPadding: PaddingValues = PaddingValues(end = 4.dp), actions: @Composable RowScope.() -> Unit = {}, ) { var shouldShowPassword by remember { mutableStateOf(initialShowPassword) } @@ -122,11 +175,12 @@ fun BitwardenPasswordFieldWithActions( singleLine = singleLine, showPasswordTestTag = showPasswordTestTag, passwordFieldTestTag = passwordFieldTestTag, + actionsPadding = actionsPadding, actions = actions, ) } -@Preview(showBackground = true) +@Preview @Composable private fun BitwardenPasswordFieldWithActions_preview() { BitwardenTheme { @@ -135,12 +189,13 @@ private fun BitwardenPasswordFieldWithActions_preview() { value = "samplePassword", onValueChange = {}, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_check_mark, contentDescription = "", onClick = {}, ) }, + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index 43cd6ce660..185d61bc1a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -1,13 +1,19 @@ package com.x8bit.bitwarden.ui.platform.components.field import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -33,12 +39,16 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupProperties +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.toPx import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth import com.x8bit.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -57,7 +67,7 @@ import kotlinx.collections.immutable.toImmutableList * the [value] is empty. * @param leadingIconResource the optional resource for the leading icon on the text field. * @param trailingIconContent the content for the trailing icon in the text field. - * @param hint optional hint text that will appear below the text input. + * @param supportingText optional supporting text that will appear below the text input. * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. * @param readOnly `true` if the input should be read-only and not accept user interactions. @@ -68,8 +78,8 @@ import kotlinx.collections.immutable.toImmutableList * @param visualTransformation Transforms the visual representation of the input [value]. * @param keyboardType the preferred type of keyboard input. * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. */ -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun BitwardenTextField( label: String?, @@ -79,7 +89,7 @@ fun BitwardenTextField( placeholder: String? = null, leadingIconResource: IconResource? = null, trailingIconContent: (@Composable () -> Unit)? = null, - hint: String? = null, + supportingText: String? = null, singleLine: Boolean = true, readOnly: Boolean = false, enabled: Boolean = true, @@ -92,6 +102,91 @@ fun BitwardenTextField( textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, autoCompleteOptions: ImmutableList = persistentListOf(), textFieldTestTag: String? = null, + cardStyle: CardStyle? = null, +) { + BitwardenTextField( + modifier = modifier, + label = label, + value = value, + onValueChange = onValueChange, + placeholder = placeholder, + leadingIconResource = leadingIconResource, + trailingIconContent = trailingIconContent, + supportingTextContent = supportingText?.let { + { + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + singleLine = singleLine, + readOnly = readOnly, + enabled = enabled, + textStyle = textStyle, + shouldAddCustomLineBreaks = shouldAddCustomLineBreaks, + keyboardType = keyboardType, + isError = isError, + autoFocus = autoFocus, + visualTransformation = visualTransformation, + textToolbarType = textToolbarType, + autoCompleteOptions = autoCompleteOptions, + textFieldTestTag = textFieldTestTag, + cardStyle = cardStyle, + ) +} + +/** + * Component that allows the user to input text. This composable will manage the state of + * the user's input. + * @param label label for the text field. + * @param value current next on the text field. + * @param modifier modifier for the composable. + * @param onValueChange callback that is triggered when the input of the text field changes. + * @param supportingTextContent An optional supporting text composable that will appear below the + * text input. + * @param placeholder the optional placeholder to be displayed when the text field is in focus and + * the [value] is empty. + * @param leadingIconResource the optional resource for the leading icon on the text field. + * @param trailingIconContent the content for the trailing icon in the text field. + * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls + * instead of wrapping onto multiple lines. + * @param readOnly `true` if the input should be read-only and not accept user interactions. + * @param enabled Whether or not the text field is enabled. + * @param textStyle An optional style that may be used to override the default used. + * @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling + * an entire line before breaking. `false` by default. + * @param visualTransformation Transforms the visual representation of the input [value]. + * @param keyboardType the preferred type of keyboard input. + * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. + */ +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun BitwardenTextField( + label: String?, + value: String, + onValueChange: (String) -> Unit, + supportingTextContent: (@Composable ColumnScope.() -> Unit)?, + modifier: Modifier = Modifier, + placeholder: String? = null, + leadingIconResource: IconResource? = null, + trailingIconContent: (@Composable () -> Unit)? = null, + singleLine: Boolean = true, + readOnly: Boolean = false, + enabled: Boolean = true, + textStyle: TextStyle = BitwardenTheme.typography.bodyLarge, + shouldAddCustomLineBreaks: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Text, + isError: Boolean = false, + autoFocus: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + autoCompleteOptions: ImmutableList = persistentListOf(), + textFieldTestTag: String? = null, + cardStyle: CardStyle? = null, ) { var widthPx by remember { mutableIntStateOf(0) } val focusRequester = remember { FocusRequester() } @@ -126,59 +221,71 @@ fun BitwardenTextField( var lastTextValue by remember(value) { mutableStateOf(value = value) } CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) { var hasFocused by remember { mutableStateOf(value = false) } - Box(modifier = modifier) { - OutlinedTextField( - colors = bitwardenTextFieldColors(), + Box(modifier = modifier.defaultMinSize(minHeight = 60.dp)) { + Column( modifier = Modifier .testTag(textFieldTestTag.orEmpty()) .onGloballyPositioned { widthPx = it.size.width } .onFocusEvent { focusState -> hasFocused = focusState.hasFocus } .focusRequester(focusRequester) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, vertical = 6.dp) .fillMaxWidth(), - enabled = enabled, - label = label?.let { { Text(text = it) } }, - value = textFieldValue, - leadingIcon = leadingIconResource?.let { iconResource -> - { - Icon( - painter = iconResource.iconPainter, - contentDescription = iconResource.contentDescription, - ) - } - }, - trailingIcon = trailingIconContent, - placeholder = placeholder?.let { - { - Text( - text = it, - style = textStyle, - ) - } - }, - supportingText = hint?.let { - { - Text( - text = hint, - style = BitwardenTheme.typography.bodySmall, - ) - } - }, - onValueChange = { - hasFocused = true - textFieldValueState = it - val stringChangedSinceLastInvocation = lastTextValue != it.text - lastTextValue = it.text - if (stringChangedSinceLastInvocation) { - onValueChange(it.text) - } - }, - singleLine = singleLine, - readOnly = readOnly, - textStyle = textStyle, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType), - isError = isError, - visualTransformation = visualTransformation, - ) + ) { + TextField( + colors = bitwardenTextFieldColors(), + enabled = enabled, + label = label?.let { { Text(text = it) } }, + value = textFieldValue, + leadingIcon = leadingIconResource?.let { iconResource -> + { + Icon( + painter = iconResource.iconPainter, + contentDescription = iconResource.contentDescription, + ) + } + }, + trailingIcon = trailingIconContent, + placeholder = placeholder?.let { + { + Text( + text = it, + style = textStyle, + ) + } + }, + onValueChange = { + hasFocused = true + textFieldValueState = it + val stringChangedSinceLastInvocation = lastTextValue != it.text + lastTextValue = it.text + if (stringChangedSinceLastInvocation) { + onValueChange(it.text) + } + }, + singleLine = singleLine, + readOnly = readOnly, + textStyle = textStyle, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType), + isError = isError, + visualTransformation = visualTransformation, + modifier = Modifier.fillMaxWidth(), + ) + supportingTextContent?.let { + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Column( + modifier = Modifier.padding(horizontal = 16.dp), + content = it, + ) + Spacer(modifier = Modifier.height(height = 6.dp)) + } + } val filteredAutoCompleteList = autoCompleteOptions .filter { option -> option.startsWith(textFieldValue.text) && option != textFieldValue.text @@ -208,22 +315,23 @@ fun BitwardenTextField( @Preview @Composable -private fun BitwardenTextField_preview_withInput() { - BitwardenTextField( - label = "Label", - value = "Input", - onValueChange = {}, - hint = "Hint", - ) -} - -@Preview -@Composable -private fun BitwardenTextField_preview_withoutInput() { - BitwardenTextField( - label = "Label", - value = "", - onValueChange = {}, - hint = "Hint", - ) +private fun BitwardenTextField_preview() { + BitwardenTheme { + Column { + BitwardenTextField( + label = "Label", + value = "Input", + onValueChange = {}, + supportingText = "Hint", + cardStyle = CardStyle.Top(), + ) + BitwardenTextField( + label = "Label", + value = "", + onValueChange = {}, + supportingText = "Hint", + cardStyle = CardStyle.Bottom, + ) + } + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextFieldWithActions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextFieldWithActions.kt index f1f49d1e42..f869d52f9a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextFieldWithActions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextFieldWithActions.kt @@ -1,9 +1,17 @@ package com.x8bit.bitwarden.ui.platform.components.field +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,7 +22,12 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -29,6 +42,8 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param value Current text in the text field. * @param onValueChange Callback that is triggered when the text content changes. * @param modifier [Modifier] applied to this layout composable. + * @param supportingText An optional supporting text that will appear below the text field and + * actions. * @param textStyle The [TextStyle], or null if default. * @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling * an entire line before breaking. `false` by default. @@ -41,9 +56,11 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param actions A lambda containing the set of actions (usually icons or similar) to display * next to the text field. This lambda extends [RowScope], * providing flexibility in the layout definition. + * @param actionsPadding Padding to be applied to the [actions] block. * @param actionsTestTag The test tag to use for the row of actions, or null if there is none. * @param textFieldTestTag The test tag to be used on the text field. * @param textToolbarType The type of [TextToolbar] to use on the text field. + * @param cardStyle Indicates the type of card style to be applied. */ @Composable fun BitwardenTextFieldWithActions( @@ -51,6 +68,7 @@ fun BitwardenTextFieldWithActions( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + supportingText: String? = null, textStyle: TextStyle = BitwardenTheme.typography.bodyLarge, shouldAddCustomLineBreaks: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, @@ -59,39 +77,65 @@ fun BitwardenTextFieldWithActions( keyboardType: KeyboardType = KeyboardType.Text, trailingIconContent: (@Composable () -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, + actionsPadding: PaddingValues = PaddingValues(end = 4.dp), actionsTestTag: String? = null, textFieldTestTag: String? = null, textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + cardStyle: CardStyle? = null, ) { - Row( + Column( modifier = modifier - .fillMaxWidth() + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, vertical = 6.dp) .semantics(mergeDescendants = true) { }, - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.Center, ) { - BitwardenTextField( - modifier = Modifier.weight(1f), - label = label, - value = value, - readOnly = readOnly, - singleLine = singleLine, - onValueChange = onValueChange, - keyboardType = keyboardType, - trailingIconContent = trailingIconContent, - textStyle = textStyle, - shouldAddCustomLineBreaks = shouldAddCustomLineBreaks, - visualTransformation = visualTransformation, - textToolbarType = textToolbarType, - textFieldTestTag = textFieldTestTag, - ) - BitwardenRowOfActions( - modifier = Modifier.run { actionsTestTag?.let { testTag(it) } ?: this }, - actions = actions, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + BitwardenTextField( + modifier = Modifier.weight(weight = 1f), + label = label, + value = value, + readOnly = readOnly, + singleLine = singleLine, + onValueChange = onValueChange, + keyboardType = keyboardType, + trailingIconContent = trailingIconContent, + textStyle = textStyle, + shouldAddCustomLineBreaks = shouldAddCustomLineBreaks, + visualTransformation = visualTransformation, + textToolbarType = textToolbarType, + textFieldTestTag = textFieldTestTag, + ) + BitwardenRowOfActions( + modifier = Modifier + .padding(paddingValues = actionsPadding) + .run { actionsTestTag?.let { testTag(it) } ?: this }, + actions = actions, + ) + } + supportingText?.let { + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + } } } -@Preview(showBackground = true) +@Preview @Composable private fun BitwardenTextFieldWithActions_preview() { BitwardenTheme { @@ -99,6 +143,7 @@ private fun BitwardenTextFieldWithActions_preview() { label = "Username", value = "user@example.com", onValueChange = {}, + supportingText = "I'm here for support", actions = { Icon( painter = rememberVectorPainter(id = R.drawable.ic_question_circle), @@ -109,6 +154,7 @@ private fun BitwardenTextFieldWithActions_preview() { contentDescription = "Action 2", ) }, + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt index 7a28861299..42dc20dc9b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt @@ -11,10 +11,10 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme */ @Composable fun bitwardenTextFieldButtonColors(): TextFieldColors = bitwardenTextFieldColors( - unfocusedBorderColor = BitwardenTheme.colorScheme.stroke.divider, - focusedBorderColor = BitwardenTheme.colorScheme.stroke.divider, + unfocusedBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent, disabledTextColor = BitwardenTheme.colorScheme.text.primary, - disabledBorderColor = BitwardenTheme.colorScheme.stroke.divider, + disabledBorderColor = Color.Transparent, disabledLeadingIconColor = BitwardenTheme.colorScheme.icon.primary, disabledTrailingIconColor = BitwardenTheme.colorScheme.icon.primary, disabledLabelColor = BitwardenTheme.colorScheme.text.secondary, @@ -27,10 +27,10 @@ fun bitwardenTextFieldButtonColors(): TextFieldColors = bitwardenTextFieldColors */ @Composable fun bitwardenTextFieldColors( - unfocusedBorderColor: Color = BitwardenTheme.colorScheme.stroke.divider, - focusedBorderColor: Color = BitwardenTheme.colorScheme.stroke.border, + focusedBorderColor: Color = Color.Transparent, + unfocusedBorderColor: Color = Color.Transparent, disabledTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled, - disabledBorderColor: Color = BitwardenTheme.colorScheme.outlineButton.borderDisabled, + disabledBorderColor: Color = Color.Transparent, disabledLeadingIconColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled, disabledTrailingIconColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled, disabledLabelColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenListHeaderText.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenListHeaderText.kt index 6bcd2c43c0..a4b7af2db7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenListHeaderText.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/header/BitwardenListHeaderText.kt @@ -1,12 +1,10 @@ package com.x8bit.bitwarden.ui.platform.components.header import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -27,10 +25,7 @@ fun BitwardenListHeaderText( text = "${label.uppercase()}$supportLabel", style = BitwardenTheme.typography.eyebrowMedium, color = BitwardenTheme.colorScheme.text.secondary, - modifier = modifier.padding( - top = 12.dp, - bottom = 4.dp, - ), + modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenGroupItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenGroupItem.kt index a19aa13095..012e0fb0a8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenGroupItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenGroupItem.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -19,8 +19,9 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider -import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -35,6 +36,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param modifier The [Modifier] to be applied to the [Row] composable that holds the list item. * @param showDivider Indicates whether the divider should be shown or not. * @param startIconTestTag The optional test tag for the [startIcon]. + * @param cardStyle Indicates the type of card style to be applied. */ @Composable fun BitwardenGroupItem( @@ -45,9 +47,12 @@ fun BitwardenGroupItem( modifier: Modifier = Modifier, showDivider: Boolean = true, startIconTestTag: String? = null, + cardStyle: CardStyle?, ) { Row( - modifier = Modifier + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple( @@ -55,16 +60,7 @@ fun BitwardenGroupItem( ), onClick = onClick, ) - .bottomDivider( - enabled = showDivider, - paddingStart = 16.dp, - ) - .padding( - top = 16.dp, - bottom = 16.dp, - end = 8.dp, - ) - .then(modifier), + .cardPadding(cardStyle = cardStyle, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { @@ -73,6 +69,7 @@ fun BitwardenGroupItem( contentDescription = null, tint = BitwardenTheme.colorScheme.icon.primary, modifier = Modifier + .defaultMinSize(minHeight = 36.dp) .semantics { startIconTestTag?.let { testTag = it } } .size(24.dp), ) @@ -89,19 +86,10 @@ fun BitwardenGroupItem( style = BitwardenTheme.typography.labelSmall, color = BitwardenTheme.colorScheme.text.primary, ) - - Icon( - painter = rememberVectorPainter(id = R.drawable.ic_chevron_right), - contentDescription = null, - tint = BitwardenTheme.colorScheme.icon.primary, - modifier = Modifier - .mirrorIfRtl() - .size(24.dp), - ) } } -@Preview(showBackground = true) +@Preview @Composable private fun BitwardenGroupItem_preview() { BitwardenTheme { @@ -111,6 +99,7 @@ private fun BitwardenGroupItem_preview() { startIcon = rememberVectorPainter(id = R.drawable.ic_file_text), startIconTestTag = "Test Tag", onClick = {}, + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt index 6597d10a22..c6d63bd2d3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon @@ -30,10 +29,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow import com.x8bit.bitwarden.ui.platform.components.icon.BitwardenIcon +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -59,6 +62,7 @@ import kotlinx.collections.immutable.persistentListOf * @param supportingLabelTestTag The optional test tag for the [supportingLabel]. * @param startIconTestTag The optional test tag for the [startIcon]. * @param trailingLabelIcons An optional list of small icons to be displayed after the [label]. + * @param cardStyle Indicates the type of card style to be applied. */ @Suppress("LongMethod") @Composable @@ -76,10 +80,13 @@ fun BitwardenListItem( supportingLabelTestTag: String? = null, startIconTestTag: String? = null, trailingLabelIcons: ImmutableList = persistentListOf(), + cardStyle: CardStyle? = null, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(false) } Row( - modifier = Modifier + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple( @@ -87,9 +94,11 @@ fun BitwardenListItem( ), onClick = onClick, ) - .defaultMinSize(minHeight = 72.dp) - .padding(vertical = 8.dp) - .then(modifier), + .cardPadding( + cardStyle = cardStyle, + start = 16.dp, + end = 4.dp, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { @@ -130,7 +139,7 @@ fun BitwardenListItem( } } - secondSupportingLabel?.let { secondSupportLabel -> + secondSupportingLabel.orNullIfBlank()?.let { secondSupportLabel -> Text( text = secondSupportLabel, style = BitwardenTheme.typography.bodyMedium, @@ -141,7 +150,7 @@ fun BitwardenListItem( ) } - supportingLabel?.let { supportLabel -> + supportingLabel.orNullIfBlank()?.let { supportLabel -> Text( text = supportLabel, style = BitwardenTheme.typography.bodyMedium, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/CardStyle.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/CardStyle.kt new file mode 100644 index 0000000000..c9656e54ab --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/model/CardStyle.kt @@ -0,0 +1,51 @@ +package com.x8bit.bitwarden.ui.platform.components.model + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Defines the possible display options for a card background on a composable component. + */ +sealed class CardStyle { + /** + * Indicates if a divider should be displayed in this card. + */ + abstract val hasDivider: Boolean + + /** + * Provides start padding to the divider when present. + */ + abstract val dividerPadding: Dp + + /** + * Defines a top card that has top rounded corners. + */ + data class Top( + override val hasDivider: Boolean = true, + override val dividerPadding: Dp = 16.dp, + ) : CardStyle() + + /** + * Defines a middle card that has no rounded corners. + */ + data class Middle( + override val hasDivider: Boolean = true, + override val dividerPadding: Dp = 16.dp, + ) : CardStyle() + + /** + * Defines a bottom card that has bottom rounded corners. + */ + data object Bottom : CardStyle() { + override val hasDivider: Boolean = false + override val dividerPadding: Dp = 0.dp + } + + /** + * Defines a full card that has rounded corners. + */ + data object Full : CardStyle() { + override val hasDivider: Boolean = false + override val dividerPadding: Dp = 0.dp + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenExternalLinkRow.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenExternalLinkRow.kt index f9b60bd887..880ff39283 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenExternalLinkRow.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenExternalLinkRow.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -31,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * user clicks this item. * @param dialogDismissButtonText The text on the dismiss button of the dialog displayed when the * user clicks this item. + * @param cardStyle Indicates the type of card style to be applied. */ @Composable fun BitwardenExternalLinkRow( @@ -43,6 +45,7 @@ fun BitwardenExternalLinkRow( dialogMessage: String, dialogConfirmButtonText: String = stringResource(id = R.string.continue_text), dialogDismissButtonText: String = stringResource(id = R.string.cancel), + cardStyle: CardStyle? = null, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(false) } BitwardenTextRow( @@ -51,6 +54,7 @@ fun BitwardenExternalLinkRow( onClick = { shouldShowDialog = true }, modifier = modifier, withDivider = withDivider, + cardStyle = cardStyle, ) { Icon( modifier = Modifier.mirrorIfRtl(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenRowOfActions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenRowOfActions.kt index c5e2fb86b5..3b015c83ac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenRowOfActions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenRowOfActions.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.platform.components.row -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding @@ -32,7 +31,6 @@ fun BitwardenRowOfActions( ) { Row( modifier = modifier.padding(start = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, content = actions, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenTextRow.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenTextRow.kt index f23d3593ce..29b6da2beb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenTextRow.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/row/BitwardenTextRow.kt @@ -15,9 +15,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -28,10 +32,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param onClick The callback when the row is clicked. * @param modifier The modifier to be applied to the layout. * @param description An optional description label to be displayed below the [text]. + * @param textTestTag The optional test tag for the inner text component. * @param isEnabled Indicates if the row is enabled or not, a disabled row will not be clickable * and it's contents will be dimmed. * @param withDivider Indicates if a divider should be drawn on the bottom of the row, defaults * to `false`. + * @param cardStyle Indicates the type of card style to be applied. * @param content The content of the [BitwardenTextRow]. */ @Composable @@ -40,13 +46,17 @@ fun BitwardenTextRow( onClick: () -> Unit, modifier: Modifier = Modifier, description: String? = null, + textTestTag: String? = null, isEnabled: Boolean = true, withDivider: Boolean = false, + cardStyle: CardStyle? = null, content: (@Composable () -> Unit)? = null, ) { Box( - contentAlignment = Alignment.BottomCenter, + contentAlignment = Alignment.CenterStart, modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( enabled = isEnabled, interactionSource = remember { MutableInteractionSource() }, @@ -55,13 +65,11 @@ fun BitwardenTextRow( ), onClick = onClick, ) + .cardPadding(cardStyle = cardStyle, horizontal = 16.dp) .semantics(mergeDescendants = true) { }, ) { Row( - modifier = Modifier - .defaultMinSize(minHeight = 56.dp) - .padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp) - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -78,6 +86,7 @@ fun BitwardenTextRow( } else { BitwardenTheme.colorScheme.filledButton.foregroundDisabled }, + modifier = Modifier.run { textTestTag?.let { testTag(it) } ?: this }, ) description?.let { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/slider/BitwardenSlider.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/slider/BitwardenSlider.kt index bd3ef006b4..7afa0de39d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/slider/BitwardenSlider.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/slider/BitwardenSlider.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.components.slider import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth @@ -22,7 +23,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent @@ -37,8 +37,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.toDp import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.slider.color.bitwardenSliderColors import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -52,6 +55,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param modifier The [Modifier] to be applied to this radio button. * @param sliderTag The option test tag for the slider component. * @param valueTag The option test tag for the value field component. + * @param cardStyle Indicates the type of card style to be applied. */ @OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod") @@ -63,13 +67,18 @@ fun BitwardenSlider( modifier: Modifier = Modifier, sliderTag: String? = null, valueTag: String? = null, + cardStyle: CardStyle? = null, ) { val sliderValue by rememberUpdatedState(newValue = value.coerceIn(range = range)) var labelTextWidth by remember { mutableStateOf(value = Dp.Unspecified) } val density = LocalDensity.current Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier.semantics(mergeDescendants = true) {}, + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, end = 16.dp) + .semantics(mergeDescendants = true) {}, ) { TextField( value = sliderValue.toString(), @@ -79,6 +88,7 @@ fun BitwardenSlider( label = { Text( text = stringResource(id = R.string.length), + style = BitwardenTheme.typography.bodySmall, modifier = Modifier.onGloballyPositioned { layoutCoordinates -> if (labelTextWidth == Dp.Unspecified) { labelTextWidth = layoutCoordinates.size.width.toDp(density = density) @@ -88,11 +98,7 @@ fun BitwardenSlider( }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - colors = bitwardenTextFieldColors( - disabledBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - ), + colors = bitwardenTextFieldColors(), modifier = Modifier .onPreviewKeyEvent { keyEvent -> when (keyEvent.key) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt index 289cf8a09d..9c103f1616 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt @@ -1,14 +1,29 @@ package com.x8bit.bitwarden.ui.platform.components.stepper +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.ZERO_WIDTH_CHARACTER +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledIconButton +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions +import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** * Displays a stepper that allows the user to increment and decrement an int value. @@ -19,24 +34,84 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithAc * @param onValueChange callback invoked when the user increments or decrements the count. Note * that this will not be called if the attempts to move value outside of [range]. * @param modifier Modifier. + * @param supportingText An optional supporting text that will appear below the stepper. * @param range Range of valid values. * @param isIncrementEnabled whether or not the increment button should be enabled. * @param isDecrementEnabled whether or not the decrement button should be enabled. * @param textFieldReadOnly whether or not the text field should be read only. The stepper * increment and decrement buttons function regardless of this value. + * @param cardStyle Indicates the type of card style to be applied. */ -@Suppress("CyclomaticComplexMethod") @Composable fun BitwardenStepper( label: String, value: Int?, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, + supportingText: String? = null, range: ClosedRange = 1..Int.MAX_VALUE, isIncrementEnabled: Boolean = true, isDecrementEnabled: Boolean = true, textFieldReadOnly: Boolean = true, stepperActionsTestTag: String? = null, + cardStyle: CardStyle? = null, +) { + BitwardenStepper( + modifier = modifier, + label = label, + value = value, + onValueChange = onValueChange, + supportingTextContent = supportingText?.let { + { + Text( + text = it, + style = BitwardenTheme.typography.bodySmall, + color = bitwardenTextFieldColors().focusedSupportingTextColor, + modifier = Modifier.fillMaxWidth(), + ) + } + }, + range = range, + isIncrementEnabled = isIncrementEnabled, + isDecrementEnabled = isDecrementEnabled, + textFieldReadOnly = textFieldReadOnly, + stepperActionsTestTag = stepperActionsTestTag, + cardStyle = cardStyle, + ) +} + +/** + * Displays a stepper that allows the user to increment and decrement an int value. + * + * @param label Label for the stepper. + * @param value Value to display. Null will display nothing. Will be clamped to [range] before + * display. + * @param onValueChange callback invoked when the user increments or decrements the count. Note + * that this will not be called if the attempts to move value outside of [range]. + * @param modifier Modifier. + * @param supportingTextContent An optional supporting text composable that will appear below the + * stepper. + * @param range Range of valid values. + * @param isIncrementEnabled whether or not the increment button should be enabled. + * @param isDecrementEnabled whether or not the decrement button should be enabled. + * @param textFieldReadOnly whether or not the text field should be read only. The stepper + * increment and decrement buttons function regardless of this value. + * @param cardStyle Indicates the type of card style to be applied. + */ +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun BitwardenStepper( + label: String, + value: Int?, + onValueChange: (Int) -> Unit, + modifier: Modifier = Modifier, + supportingTextContent: @Composable (ColumnScope.() -> Unit)?, + range: ClosedRange = 1..Int.MAX_VALUE, + isIncrementEnabled: Boolean = true, + isDecrementEnabled: Boolean = true, + textFieldReadOnly: Boolean = true, + stepperActionsTestTag: String? = null, + cardStyle: CardStyle? = null, ) { val clampedValue = value?.coerceIn(range) if (clampedValue != value && clampedValue != null) { @@ -44,50 +119,67 @@ fun BitwardenStepper( } val isAtRangeMinimum = clampedValue?.let { (it - 1) !in range } ?: true val isAtRangeMaximum = clampedValue?.let { (it + 1) !in range } ?: false - BitwardenTextFieldWithActions( - label = label, - // We use the zero width character instead of an empty string to make sure label is shown - // small and above the input - value = clampedValue?.toString() ?: ZERO_WIDTH_CHARACTER, - actionsTestTag = stepperActionsTestTag, - actions = { - BitwardenFilledIconButton( - vectorIconRes = R.drawable.ic_minus, - contentDescription = "\u2212", - onClick = { - val decrementedValue = ((clampedValue ?: 0) - 1).coerceIn(range) - if (decrementedValue != clampedValue) { - onValueChange(decrementedValue) - } - }, - isEnabled = isDecrementEnabled && !isAtRangeMinimum, - modifier = Modifier.testTag("DecrementValue"), + Column( + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, vertical = 8.dp), + ) { + BitwardenTextFieldWithActions( + label = label, + value = clampedValue?.toString().orEmpty(), + actionsTestTag = stepperActionsTestTag, + actions = { + BitwardenFilledIconButton( + vectorIconRes = R.drawable.ic_minus, + contentDescription = "\u2212", + onClick = { + val decrementedValue = ((clampedValue ?: 0) - 1).coerceIn(range) + if (decrementedValue != clampedValue) { + onValueChange(decrementedValue) + } + }, + isEnabled = isDecrementEnabled && !isAtRangeMinimum, + modifier = Modifier.testTag("DecrementValue"), + ) + BitwardenFilledIconButton( + vectorIconRes = R.drawable.ic_plus, + contentDescription = "+", + onClick = { + val incrementedValue = ((clampedValue ?: 0) + 1).coerceIn(range) + if (incrementedValue != clampedValue) { + onValueChange(incrementedValue) + } + }, + isEnabled = isIncrementEnabled && !isAtRangeMaximum, + modifier = Modifier.testTag("IncrementValue"), + ) + }, + readOnly = textFieldReadOnly, + keyboardType = KeyboardType.Number, + onValueChange = { newValue -> + onValueChange( + newValue + .orNullIfBlank() + ?.let { it.toIntOrNull()?.coerceIn(range) ?: clampedValue } + ?: range.start, + ) + }, + actionsPadding = PaddingValues(end = 12.dp), + ) + supportingTextContent?.let { + Spacer(modifier = Modifier.height(height = 8.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), ) - BitwardenFilledIconButton( - vectorIconRes = R.drawable.ic_plus, - contentDescription = "+", - onClick = { - val incrementedValue = ((clampedValue ?: 0) + 1).coerceIn(range) - if (incrementedValue != clampedValue) { - onValueChange(incrementedValue) - } - }, - isEnabled = isIncrementEnabled && !isAtRangeMaximum, - modifier = Modifier.testTag("IncrementValue"), + Spacer(modifier = Modifier.height(height = 12.dp)) + Column( + modifier = Modifier.padding(horizontal = 16.dp), + content = it, ) - }, - readOnly = textFieldReadOnly, - keyboardType = KeyboardType.Number, - onValueChange = { newValue -> - onValueChange( - newValue - // Make sure the placeholder is gone, since it will mess up the int conversion - .replace(ZERO_WIDTH_CHARACTER, "") - .orNullIfBlank() - ?.let { it.toIntOrNull()?.coerceIn(range) ?: clampedValue } - ?: range.start, - ) - }, - modifier = modifier, - ) + Spacer(modifier = Modifier.height(height = 4.dp)) + } + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt index e569cd3e05..b3fa085d94 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt @@ -34,6 +34,7 @@ fun BitwardenClickableText( style: TextStyle, modifier: Modifier = Modifier, innerPadding: PaddingValues = PaddingValues(vertical = 4.dp, horizontal = 16.dp), + isEnabled: Boolean = true, cornerSize: Dp = 28.dp, color: Color = BitwardenTheme.colorScheme.text.interaction, ) { @@ -46,6 +47,7 @@ fun BitwardenClickableText( color = BitwardenTheme.colorScheme.background.pressed, ), interactionSource = remember { MutableInteractionSource() }, + enabled = isEnabled, onClick = onClick, ) .padding(innerPadding), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt index f82ee2c322..b6b651d0ce 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt @@ -2,14 +2,17 @@ package com.x8bit.bitwarden.ui.platform.components.toggle import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -26,8 +29,12 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -38,11 +45,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param isChecked The current state of the switch (either checked or unchecked). * @param onCheckedChange A lambda that is invoked when the switch's state changes. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. - * @param description An optional description label to be displayed below the [label]. + * @param supportingText An optional supporting text to be displayed below the [label]. * @param contentDescription A description of the switch's UI for accessibility purposes. * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. + * @param cardStyle Indicates the type of card style to be applied. * @param actions A lambda containing the set of actions (usually icons or similar) to display * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. @@ -53,10 +61,11 @@ fun BitwardenSwitch( isChecked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, - description: String? = null, + supportingText: String? = null, contentDescription: String? = null, readOnly: Boolean = false, enabled: Boolean = true, + cardStyle: CardStyle? = null, actions: (@Composable RowScope.() -> Unit)? = null, ) { BitwardenSwitch( @@ -67,9 +76,10 @@ fun BitwardenSwitch( contentDescription = contentDescription, readOnly = readOnly, enabled = enabled, + cardStyle = cardStyle, actions = actions, - subContent = { - description?.let { + supportingTextContent = supportingText?.let { + { Text( text = it, style = BitwardenTheme.typography.bodyMedium, @@ -91,11 +101,12 @@ fun BitwardenSwitch( * @param isChecked The current state of the switch (either checked or unchecked). * @param onCheckedChange A lambda that is invoked when the switch's state changes. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. - * @param description An optional description label to be displayed below the [label]. + * @param supportingText An optional supporting text to be displayed below the [label]. * @param contentDescription A description of the switch's UI for accessibility purposes. * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. + * @param cardStyle Indicates the type of card style to be applied. * @param actions A lambda containing the set of actions (usually icons or similar) to display * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. @@ -106,10 +117,11 @@ fun BitwardenSwitch( isChecked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, - description: String? = null, + supportingText: String? = null, contentDescription: String? = null, readOnly: Boolean = false, enabled: Boolean = true, + cardStyle: CardStyle? = null, actions: (@Composable RowScope.() -> Unit)? = null, ) { BitwardenSwitch( @@ -120,9 +132,10 @@ fun BitwardenSwitch( contentDescription = contentDescription, readOnly = readOnly, enabled = enabled, + cardStyle = cardStyle, actions = actions, - subContent = { - description?.let { + supportingTextContent = supportingText?.let { + { Text( text = it, style = BitwardenTheme.typography.bodyMedium, @@ -148,10 +161,11 @@ fun BitwardenSwitch( * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. + * @param cardStyle Indicates the type of card style to be applied. * @param actions A lambda containing the set of actions (usually icons or similar) to display * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. - * @param subContent A lambda containing content directly below the label. + * @param supportingTextContent A lambda containing content directly below the label. */ @Composable fun BitwardenSwitch( @@ -162,8 +176,9 @@ fun BitwardenSwitch( contentDescription: String? = null, readOnly: Boolean = false, enabled: Boolean = true, + cardStyle: CardStyle? = null, actions: (@Composable RowScope.() -> Unit)? = null, - subContent: @Composable () -> Unit, + supportingTextContent: (@Composable ColumnScope.() -> Unit)?, ) { BitwardenSwitch( modifier = modifier, @@ -173,8 +188,9 @@ fun BitwardenSwitch( contentDescription = contentDescription, readOnly = readOnly, enabled = enabled, + cardStyle = cardStyle, actions = actions, - subContent = subContent, + supportingTextContent = supportingTextContent, ) } @@ -189,10 +205,11 @@ fun BitwardenSwitch( * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. + * @param cardStyle Indicates the type of card style to be applied. * @param actions A lambda containing the set of actions (usually icons or similar) to display * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. - * @param subContent A lambda containing content directly below the label. + * @param supportingTextContent A lambda containing content directly below the label. */ @Suppress("LongMethod") @Composable @@ -204,35 +221,44 @@ fun BitwardenSwitch( contentDescription: String? = null, readOnly: Boolean = false, enabled: Boolean = true, + cardStyle: CardStyle? = null, actions: (@Composable RowScope.() -> Unit)? = null, - subContent: @Composable () -> Unit, + supportingTextContent: @Composable (ColumnScope.() -> Unit)?, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .wrapContentHeight() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = { onCheckedChange?.invoke(!isChecked) }, - enabled = !readOnly && enabled, - ) + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .run { + onCheckedChange + ?.let { + this.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple( + color = BitwardenTheme.colorScheme.background.pressed, + ), + onClick = { it(!isChecked) }, + enabled = !readOnly && enabled, + ) + } + ?: this + } + .cardPadding(cardStyle = cardStyle) .semantics(mergeDescendants = true) { toggleableState = ToggleableState(isChecked) contentDescription?.let { this.contentDescription = it } - } - .then(modifier), + }, ) { Row( - modifier = Modifier.weight(weight = 1f), verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .defaultMinSize(minHeight = 36.dp) + .padding(horizontal = 16.dp), ) { - Column( - modifier = Modifier - .weight(weight = 1f, fill = false) - .padding(vertical = 8.dp), + Row( + modifier = Modifier.weight(weight = 1f), + verticalAlignment = Alignment.CenterVertically, ) { Text( text = label, @@ -244,23 +270,33 @@ fun BitwardenSwitch( }, modifier = Modifier.testTag(tag = "SwitchText"), ) - subContent() + + actions?.invoke(this) } - - actions - ?.invoke(this) - ?: Spacer(modifier = Modifier.width(width = 16.dp)) + Spacer(modifier = Modifier.width(width = 16.dp)) + Switch( + modifier = Modifier + .defaultMinSize(minHeight = 48.dp) + .testTag(tag = "SwitchToggle"), + enabled = enabled, + checked = isChecked, + onCheckedChange = null, + colors = bitwardenSwitchColors(), + ) + } + supportingTextContent?.let { + Spacer(modifier = Modifier.height(height = 12.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + Column( + modifier = Modifier.padding(horizontal = 16.dp), + content = it, + ) } - - Switch( - modifier = Modifier - .height(height = 56.dp) - .testTag(tag = "SwitchToggle"), - enabled = enabled, - checked = isChecked, - onCheckedChange = null, - colors = bitwardenSwitchColors(), - ) } } @@ -270,7 +306,7 @@ private fun BitwardenSwitch_preview() { Column { BitwardenSwitch( label = "Label", - description = "description", + supportingText = "description", isChecked = true, onCheckedChange = {}, ) @@ -281,7 +317,7 @@ private fun BitwardenSwitch_preview() { ) BitwardenSwitch( label = "Label", - description = "description", + supportingText = "description", isChecked = true, onCheckedChange = {}, actions = { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithBiometricsSwitch.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithBiometricsSwitch.kt index 2df2eb5eb1..4a26e9cf11 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithBiometricsSwitch.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithBiometricsSwitch.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricSupportStatus /** @@ -21,6 +22,7 @@ fun BitwardenUnlockWithBiometricsSwitch( isChecked: Boolean, onDisableBiometrics: () -> Unit, onEnableBiometrics: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { val biometricsDescription: String = when (biometricSupportStatus) { @@ -49,6 +51,7 @@ fun BitwardenUnlockWithBiometricsSwitch( } }, enabled = biometricSupportStatus == BiometricSupportStatus.CLASS_3_SUPPORTED, - description = biometricsDescription, + supportingText = biometricsDescription, + cardStyle = cardStyle, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithPinSwitch.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithPinSwitch.kt index c751f7c978..43662f9f8f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithPinSwitch.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenUnlockWithPinSwitch.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInputDialog /** @@ -19,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInput * @param isUnlockWithPinEnabled Indicates whether or not the pin unlocking is enabled. * @param onUnlockWithPinToggleAction Callback that is invoked when the current state of the switch * changes. + * @param cardStyle Indicates the type of card style to be applied. * @param modifier The [Modifier] to be applied to the switch. */ @Suppress("LongMethod") @@ -27,6 +29,7 @@ fun BitwardenUnlockWithPinSwitch( isUnlockWithPasswordEnabled: Boolean, isUnlockWithPinEnabled: Boolean, onUnlockWithPinToggleAction: (UnlockWithPinState) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { var shouldShowPinInputDialog by rememberSaveable { mutableStateOf(value = false) } @@ -43,6 +46,7 @@ fun BitwardenUnlockWithPinSwitch( onUnlockWithPinToggleAction(UnlockWithPinState.Disabled) } }, + cardStyle = cardStyle, modifier = modifier, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt index e20a156c1b..8083d84b45 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -27,6 +28,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton @@ -78,7 +80,7 @@ fun DebugMenuScreen( Column( modifier = Modifier.verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) FeatureFlagContent( featureFlagMap = state.featureFlags, onValueChange = remember(viewModel) { @@ -92,7 +94,9 @@ fun DebugMenuScreen( } }, ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(height = 16.dp)) + BitwardenHorizontalDivider() + Spacer(Modifier.height(height = 16.dp)) // Pulled these into variable to avoid over-nested formatting in the composable call. val isRestartOnboardingEnabled = state.featureFlags[FlagKey.OnboardingFlow] as? Boolean val isRestartOnboardingCarouselEnabled = state @@ -111,6 +115,7 @@ fun DebugMenuScreen( } }, ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -126,22 +131,26 @@ private fun FeatureFlagContent( Column( modifier = modifier, ) { - Spacer(modifier = Modifier.height(8.dp)) BitwardenListHeaderText( label = stringResource(R.string.feature_flags), - modifier = Modifier.standardHorizontalMargin(), + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenHorizontalDivider() featureFlagMap.forEach { featureFlag -> featureFlag.key.ListItemContent( currentValue = featureFlag.value, onValueChange = onValueChange, - modifier = Modifier.standardHorizontalMargin(), + cardStyle = featureFlagMap.keys.toListItemCardStyle( + index = featureFlagMap.keys.indexOf(element = featureFlag.key), + ), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), ) - BitwardenHorizontalDivider() } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenFilledButton( label = stringResource(R.string.reset_values), onClick = onResetValues, @@ -149,7 +158,6 @@ private fun FeatureFlagContent( .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) } } @@ -167,11 +175,11 @@ private fun OnboardingOverrideContent( Column(modifier) { BitwardenListHeaderText( label = stringResource(R.string.onboarding_override), - modifier = Modifier.standardHorizontalMargin(), + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenHorizontalDivider() - Spacer(modifier = Modifier.height(12.dp)) BitwardenFilledButton( label = stringResource(R.string.restart_onboarding_cta), onClick = onStartOnboarding, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index 8e8f75ed67..1ebafe78e6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.manager.model.FlagKey +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch /** @@ -15,6 +16,7 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch fun FlagKey.ListItemContent( currentValue: T, onValueChange: (key: FlagKey, value: T) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) = when (val flagKey = this) { FlagKey.DummyBoolean, @@ -41,6 +43,7 @@ fun FlagKey.ListItemContent( key = flagKey as FlagKey, currentValue = currentValue as Boolean, onValueChange = onValueChange as (FlagKey, Boolean) -> Unit, + cardStyle = cardStyle, modifier = modifier, ) } @@ -54,14 +57,14 @@ private fun BooleanFlagItem( key: FlagKey, currentValue: Boolean, onValueChange: (key: FlagKey, value: Boolean) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { BitwardenSwitch( label = label, isChecked = currentValue, - onCheckedChange = { - onValueChange(key, it) - }, + onCheckedChange = { onValueChange(key, it) }, + cardStyle = cardStyle, modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt index 0b102942ea..873bd321e4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt @@ -2,10 +2,10 @@ package com.x8bit.bitwarden.ui.platform.feature.search import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,6 +16,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog @@ -107,7 +109,10 @@ fun SearchContent( LazyColumn( modifier = modifier, ) { - items(viewState.displayItems) { + item { + Spacer(modifier = Modifier.height(height = 12.dp)) + } + itemsIndexed(viewState.displayItems) { index, it -> BitwardenListItem( startIcon = it.iconData, label = it.title, @@ -158,19 +163,19 @@ fun SearchContent( ) } .toPersistentList(), + cardStyle = viewState.displayItems.toListItemCardStyle( + index = index, + dividerPadding = 56.dp, + ), modifier = Modifier .testTag(searchType.searchItemTestTag) .fillMaxWidth() - .padding( - start = 16.dp, - // There is some built-in padding to the menu button that makes up - // the visual difference here. - end = 12.dp, - ), + .standardHorizontalMargin(), ) } item { + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt index b21c51e384..dc3800a647 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize 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.layout.width @@ -36,10 +37,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.Text -import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -86,20 +91,22 @@ fun SettingsScreen( .fillMaxSize() .verticalScroll(state = rememberScrollState()), ) { - Settings.entries.forEach { settingEntry -> + Spacer(modifier = Modifier.height(height = 12.dp)) + Settings.entries.forEachIndexed { index, settingEntry -> SettingsRow( text = settingEntry.text, onClick = remember(viewModel) { { viewModel.trySendAction(SettingsAction.SettingsClick(settingEntry)) } }, - modifier = Modifier - .testTag(settingEntry.testTag) - .padding(horizontal = 16.dp) - .fillMaxWidth(), notificationCount = state.notificationBadgeCountMap.getOrDefault( key = settingEntry, defaultValue = 0, ), + cardStyle = Settings.entries.toListItemCardStyle(index = index), + modifier = Modifier + .testTag(tag = settingEntry.testTag) + .standardHorizontalMargin() + .fillMaxWidth(), ) } } @@ -110,11 +117,14 @@ fun SettingsScreen( private fun SettingsRow( text: Text, onClick: () -> Unit, - modifier: Modifier = Modifier, notificationCount: Int, + cardStyle: CardStyle?, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple( @@ -122,17 +132,14 @@ private fun SettingsRow( ), onClick = onClick, ) - .bottomDivider(paddingStart = 16.dp) - .defaultMinSize(minHeight = 56.dp) - .padding(end = 8.dp, top = 8.dp, bottom = 8.dp) - .then(modifier), + .cardPadding(cardStyle = cardStyle, start = 16.dp, end = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( modifier = Modifier .padding(end = 16.dp) - .weight(1f), + .weight(weight = 1f), text = text(), style = BitwardenTheme.typography.bodyLarge, color = BitwardenTheme.colorScheme.text.primary, @@ -164,7 +171,7 @@ private fun TrailingContent( tint = BitwardenTheme.colorScheme.icon.primary, modifier = Modifier .mirrorIfRtl() - .size(24.dp), + .size(size = 16.dp), ) } } @@ -185,6 +192,7 @@ private fun SettingsRows_preview() { text = it.text, onClick = { }, notificationCount = index % 3, + cardStyle = Settings.entries.toListItemCardStyle(index = index), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt index 3de12b6561..e90899f607 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt @@ -1,11 +1,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.about -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize @@ -19,18 +15,14 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri @@ -40,9 +32,12 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar -import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -161,72 +156,104 @@ private fun ContentColumn( modifier = modifier .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) if (state.shouldShowCrashLogsButton) { BitwardenSwitch( label = stringResource(id = R.string.submit_crash_logs), + contentDescription = stringResource(id = R.string.submit_crash_logs), isChecked = state.isSubmitCrashLogsEnabled, onCheckedChange = onSubmitCrashLogsCheckedChange, + cardStyle = CardStyle.Top(), modifier = Modifier .testTag("SubmitCrashLogsSwitch") .fillMaxWidth() - .padding(horizontal = 16.dp), - contentDescription = stringResource(id = R.string.submit_crash_logs), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) } BitwardenExternalLinkRow( text = stringResource(id = R.string.bitwarden_help_center), onConfirmClick = onHelpCenterClick, dialogTitle = stringResource(id = R.string.continue_to_help_center), - modifier = Modifier.testTag("BitwardenHelpCenterRow"), dialogMessage = stringResource( id = R.string.learn_more_about_how_to_use_bitwarden_on_the_help_center, ), + withDivider = false, + cardStyle = if (state.shouldShowCrashLogsButton) { + CardStyle.Middle() + } else { + CardStyle.Top() + }, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .testTag(tag = "BitwardenHelpCenterRow"), ) BitwardenExternalLinkRow( text = stringResource(id = R.string.privacy_policy), onConfirmClick = onPrivacyPolicyClick, dialogTitle = stringResource(id = R.string.continue_to_privacy_policy), - modifier = Modifier.testTag("PrivacyPolicyRow"), dialogMessage = stringResource( id = R.string.privacy_policy_description_long, ), + withDivider = false, + cardStyle = CardStyle.Middle(), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .testTag(tag = "PrivacyPolicyRow"), ) BitwardenExternalLinkRow( text = stringResource(id = R.string.web_vault), onConfirmClick = onWebVaultClick, - modifier = Modifier.testTag("BitwardenWebVaultRow"), dialogTitle = stringResource(id = R.string.continue_to_web_app), dialogMessage = stringResource( id = R.string.explore_more_features_of_your_bitwarden_account_on_the_web_app, ), + withDivider = false, + cardStyle = CardStyle.Middle(), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .testTag(tag = "BitwardenWebVaultRow"), ) BitwardenExternalLinkRow( text = stringResource(id = R.string.learn_org), onConfirmClick = onLearnAboutOrgsClick, dialogTitle = stringResource(id = R.string.continue_to_x, "bitwarden.com"), - modifier = Modifier.testTag("LearnAboutOrganizationsRow"), dialogMessage = stringResource( id = R.string.learn_about_organizations_description_long, ), + withDivider = false, + cardStyle = CardStyle.Middle(), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .testTag(tag = "LearnAboutOrganizationsRow"), ) BitwardenExternalLinkRow( text = stringResource(R.string.give_feedback), onConfirmClick = onGiveFeedbackClick, - modifier = Modifier.testTag("GiveFeedbackRow"), dialogTitle = stringResource(R.string.continue_to_give_feedback), dialogMessage = stringResource(R.string.continue_to_provide_feedback), + withDivider = false, + cardStyle = CardStyle.Middle(), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .testTag(tag = "GiveFeedbackRow"), ) CopyRow( text = state.version, onClick = onVersionClick, - modifier = Modifier.testTag("CopyAboutInfoRow"), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth() + .testTag("CopyAboutInfoRow"), ) Box( modifier = Modifier - .defaultMinSize(minHeight = 56.dp) - .padding(horizontal = 16.dp, vertical = 8.dp) + .defaultMinSize(minHeight = 60.dp) + .standardHorizontalMargin() .fillMaxWidth(), contentAlignment = Alignment.Center, ) { @@ -246,44 +273,19 @@ private fun CopyRow( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val resources = LocalContext.current.resources - Box( - contentAlignment = Alignment.BottomCenter, - modifier = modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = onClick, - ) - .semantics(mergeDescendants = true) { - contentDescription = text.toString(resources) - }, + BitwardenTextRow( + text = text(), + onClick = onClick, + withDivider = false, + cardStyle = CardStyle.Bottom, + modifier = modifier, ) { - Row( - modifier = Modifier - .defaultMinSize(minHeight = 56.dp) - .padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .padding(end = 16.dp) - .weight(1f), - text = text(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - Icon( - painter = rememberVectorPainter(id = R.drawable.ic_copy), - contentDescription = null, - tint = BitwardenTheme.colorScheme.icon.primary, - ) - } - BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + Icon( + painter = rememberVectorPainter(id = R.drawable.ic_copy), + contentDescription = null, + tint = BitwardenTheme.colorScheme.icon.primary, + modifier = Modifier.mirrorIfRtl(), + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 86c7df8afa..8ba9ba91cb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -51,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDial import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold @@ -177,6 +179,7 @@ fun AccountSecurityScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) AnimatedVisibility( visible = state.shouldShowUnlockActionCard, label = "UnlockActionCard", @@ -200,7 +203,7 @@ fun AccountSecurityScreen( }, modifier = Modifier .standardHorizontalMargin() - .padding(top = 12.dp, bottom = 16.dp), + .padding(bottom = 16.dp), ) } @@ -208,15 +211,19 @@ fun AccountSecurityScreen( label = stringResource(id = R.string.approve_login_requests), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextRow( text = stringResource(id = R.string.pending_log_in_requests), onClick = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick) } }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("PendingLogInRequestsLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -225,10 +232,14 @@ fun AccountSecurityScreen( label = stringResource(id = R.string.unlock_options), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) + + val biometricSupportStatus = biometricsManager.biometricSupportStatus BitwardenUnlockWithBiometricsSwitch( - biometricSupportStatus = biometricsManager.biometricSupportStatus, + biometricSupportStatus = biometricSupportStatus, isChecked = state.isUnlockWithBiometricsEnabled || showBiometricsPrompt, onDisableBiometrics = remember(viewModel) { { @@ -240,21 +251,24 @@ fun AccountSecurityScreen( onEnableBiometrics = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.EnableBiometricsClick) } }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("UnlockWithBiometricsSwitch") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenUnlockWithPinSwitch( isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled, isUnlockWithPinEnabled = state.isUnlockWithPinEnabled, onUnlockWithPinToggleAction = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle(it)) } }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("UnlockWithPinSwitch") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) Spacer(Modifier.height(16.dp)) if (state.shouldShowEnableAuthenticatorSync) { @@ -274,14 +288,16 @@ fun AccountSecurityScreen( label = stringResource(id = R.string.session_timeout), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) SessionTimeoutPolicyRow( vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes, vaultTimeoutPolicyAction = state.vaultTimeoutPolicyAction, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) SessionTimeoutRow( vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes, @@ -291,7 +307,8 @@ fun AccountSecurityScreen( }, modifier = Modifier .testTag("VaultTimeoutChooser") - .fillMaxWidth(), + .fillMaxWidth() + .standardHorizontalMargin(), ) (state.vaultTimeout as? VaultTimeout.Custom)?.let { customTimeout -> SessionCustomTimeoutRow( @@ -304,7 +321,9 @@ fun AccountSecurityScreen( ) } }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } SessionTimeoutActionRow( @@ -316,7 +335,8 @@ fun AccountSecurityScreen( }, modifier = Modifier .testTag("VaultTimeoutActionChooser") - .fillMaxWidth(), + .fillMaxWidth() + .standardHorizontalMargin(), ) Spacer(Modifier.height(16.dp)) @@ -324,15 +344,19 @@ fun AccountSecurityScreen( label = stringResource(id = R.string.other), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextRow( text = stringResource(id = R.string.account_fingerprint_phrase), onClick = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.AccountFingerprintPhraseClick) } }, + cardStyle = CardStyle.Top(), modifier = Modifier .testTag("AccountFingerprintPhraseLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) BitwardenExternalLinkRow( @@ -343,8 +367,10 @@ fun AccountSecurityScreen( withDivider = false, dialogTitle = stringResource(id = R.string.continue_to_web_app), dialogMessage = stringResource(id = R.string.two_step_login_description_long), + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("TwoStepLoginLinkItemView") + .standardHorizontalMargin() .fillMaxWidth(), ) if (state.isUnlockWithPasswordEnabled) { @@ -358,7 +384,10 @@ fun AccountSecurityScreen( dialogMessage = stringResource( id = R.string.change_master_password_description_long, ), - modifier = Modifier.fillMaxWidth(), + cardStyle = CardStyle.Middle(), + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), ) } if (state.hasUnlockMechanism) { @@ -367,8 +396,10 @@ fun AccountSecurityScreen( onClick = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.LockNowClick) } }, + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("LockNowLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) } @@ -377,8 +408,10 @@ fun AccountSecurityScreen( onClick = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.LogoutClick) } }, + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("LogOutLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) BitwardenTextRow( @@ -386,10 +419,13 @@ fun AccountSecurityScreen( onClick = remember(viewModel) { { viewModel.trySendAction(AccountSecurityAction.DeleteAccountClick) } }, + cardStyle = CardStyle.Bottom, modifier = Modifier .testTag("DeleteAccountLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) } } } @@ -474,6 +510,7 @@ private fun SessionTimeoutRow( BitwardenTextRow( text = stringResource(id = R.string.session_timeout), onClick = { shouldShowSelectionDialog = true }, + cardStyle = CardStyle.Top(), modifier = modifier, ) { Text( @@ -545,6 +582,7 @@ private fun SessionCustomTimeoutRow( BitwardenTextRow( text = stringResource(id = R.string.custom), onClick = { shouldShowTimePickerDialog = true }, + cardStyle = CardStyle.Middle(), modifier = modifier, ) { val formattedTime = LocalTime @@ -625,6 +663,7 @@ private fun SessionTimeoutActionRow( if (vaultTimeoutPolicyAction != null) return@BitwardenTextRow shouldShowSelectionDialog = true }, + cardStyle = CardStyle.Bottom, modifier = modifier, ) { Text( @@ -685,7 +724,7 @@ private fun SessionTimeoutActionRow( } @Composable -private fun SyncWithAuthenticatorRow( +private fun ColumnScope.SyncWithAuthenticatorRow( isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, ) { @@ -693,12 +732,15 @@ private fun SyncWithAuthenticatorRow( label = stringResource(R.string.authenticator_sync), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(R.string.allow_bitwarden_authenticator_syncing), onCheckedChange = onCheckedChange, isChecked = isChecked, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/PinInputDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/PinInputDialog.kt index 304eb5e532..dd4e09c69a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/PinInputDialog.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/PinInputDialog.kt @@ -34,6 +34,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -115,9 +116,9 @@ fun PinInputDialog( pin = newValue.filter { it.isDigit() || !isPinCreation } }, keyboardType = KeyboardType.Number, - modifier = Modifier - .fillMaxWidth(), textFieldTestTag = "AlertInputField", + cardStyle = CardStyle.Full, + modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(height = 16.dp)) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt index 28b7d7e109..d4fb3985aa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt @@ -11,15 +11,16 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -49,8 +50,10 @@ import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow import com.x8bit.bitwarden.data.platform.util.isFdroid import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect -import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.bottomsheet.BitwardenModalBottomSheet import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton @@ -58,6 +61,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -223,20 +227,27 @@ private fun PendingRequestsContent( LazyColumn( modifier = Modifier.weight(weight = 1f, fill = false), ) { - items(state.requests) { request -> + item { + Spacer(modifier = Modifier.height(height = 12.dp)) + } + itemsIndexed(state.requests) { index, request -> PendingRequestItem( fingerprintPhrase = request.fingerprintPhrase, platform = request.platform, timestamp = request.timestamp, onNavigateToLoginApproval = onNavigateToLoginApproval, + cardStyle = state.requests.toListItemCardStyle( + index = index, + dividerPadding = 0.dp, + ), modifier = Modifier .testTag("LoginRequestCell") .fillMaxWidth() - .bottomDivider(), + .standardHorizontalMargin(), ) } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 24.dp)) BitwardenOutlinedButton( label = stringResource(id = R.string.decline_all_requests), @@ -244,9 +255,11 @@ private fun PendingRequestsContent( onClick = { shouldShowDeclineAllRequestsConfirm = true }, modifier = Modifier .testTag("DeclineAllRequestsButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) + + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -260,29 +273,29 @@ private fun PendingRequestItem( platform: String, timestamp: String, onNavigateToLoginApproval: (fingerprintPhrase: String) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { Column( modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple( color = BitwardenTheme.colorScheme.background.pressed, ), onClick = { onNavigateToLoginApproval(fingerprintPhrase) }, - ), + ) + .cardPadding(cardStyle = cardStyle, horizontal = 16.dp), horizontalAlignment = Alignment.Start, ) { - Spacer(modifier = Modifier.height(16.dp)) - Text( text = stringResource(id = R.string.fingerprint_phrase), style = BitwardenTheme.typography.labelMedium, color = BitwardenTheme.colorScheme.text.primary, textAlign = TextAlign.Start, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(8.dp)) @@ -294,8 +307,7 @@ private fun PendingRequestItem( textAlign = TextAlign.Start, modifier = Modifier .testTag("FingerprintValueLabel") - .fillMaxWidth() - .padding(horizontal = 16.dp), + .fillMaxWidth(), ) Spacer(modifier = Modifier.height(12.dp)) @@ -310,18 +322,15 @@ private fun PendingRequestItem( style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.secondary, textAlign = TextAlign.Start, - modifier = Modifier.padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.width(width = 16.dp)) Text( text = timestamp, style = BitwardenTheme.typography.labelSmall, color = BitwardenTheme.colorScheme.text.secondary, textAlign = TextAlign.End, - modifier = Modifier.padding(horizontal = 16.dp), ) } - - Spacer(modifier = Modifier.height(16.dp)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt index c104d7a1fe..3e77212ed1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt @@ -1,9 +1,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.appearance import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -26,10 +28,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -78,6 +82,7 @@ fun AppearanceScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) LanguageSelectionRow( currentSelection = state.language, onLanguageSelection = remember(viewModel) { @@ -85,9 +90,10 @@ fun AppearanceScreen( }, modifier = Modifier .testTag("LanguageChooser") + .standardHorizontalMargin() .fillMaxWidth(), ) - + Spacer(modifier = Modifier.height(height = 8.dp)) ThemeSelectionRow( currentSelection = state.theme, onThemeSelection = remember(viewModel) { @@ -95,21 +101,24 @@ fun AppearanceScreen( }, modifier = Modifier .testTag("ThemeChooser") + .standardHorizontalMargin() .fillMaxWidth(), ) - + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(id = R.string.show_website_icons), - description = stringResource(id = R.string.show_website_icons_description), + supportingText = stringResource(id = R.string.show_website_icons_description), isChecked = state.showWebsiteIcons, onCheckedChange = remember(viewModel) { { viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsToggle(it)) } }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("ShowWebsiteIconsSwitch") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } @@ -133,6 +142,7 @@ private fun LanguageSelectionRow( BitwardenTextRow( text = stringResource(id = R.string.language), onClick = { shouldShowLanguageSelectionDialog = true }, + cardStyle = CardStyle.Full, modifier = modifier, ) { Text( @@ -174,6 +184,7 @@ private fun ThemeSelectionRow( text = stringResource(id = R.string.theme), description = stringResource(id = R.string.theme_description), onClick = { shouldShowThemeSelectionDialog = true }, + cardStyle = CardStyle.Full, modifier = modifier, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index ffeeafff85..4d50e61cfa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -41,6 +41,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialo import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold @@ -128,6 +129,7 @@ fun AutoFillScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) AnimatedVisibility( visible = state.showAutofillActionCard, label = "AutofillActionCard", @@ -151,31 +153,35 @@ fun AutoFillScreen( }, modifier = Modifier .standardHorizontalMargin() - .padding(top = 12.dp, bottom = 16.dp), + .padding(bottom = 16.dp), ) } BitwardenListHeaderText( label = stringResource(id = R.string.autofill), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(id = R.string.autofill_services), - description = stringResource(id = R.string.autofill_services_explanation_long), + supportingText = stringResource(id = R.string.autofill_services_explanation_long), isChecked = state.isAutoFillServicesEnabled, onCheckedChange = remember(viewModel) { { viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(it)) } }, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("AutofillServicesSwitch") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) if (state.showInlineAutofillOption) { BitwardenSwitch( label = stringResource(id = R.string.inline_autofill), - description = stringResource( + supportingText = stringResource( id = R.string.use_inline_autofill_explanation_long, ), isChecked = state.isUseInlineAutoFillEnabled, @@ -183,11 +189,13 @@ fun AutoFillScreen( { viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(it)) } }, enabled = state.canInteractWithInlineAutofillToggle, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("InlineAutofillSwitch") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } if (state.showPasskeyManagementRow) { BitwardenExternalLinkRow( @@ -203,7 +211,12 @@ fun AutoFillScreen( id = R.string.set_bitwarden_as_passkey_manager_description, ), withDivider = false, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } AccessibilityAutofillSwitch( isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled, @@ -212,39 +225,45 @@ fun AutoFillScreen( }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.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)) BitwardenSwitch( label = stringResource(id = R.string.copy_totp_automatically), - description = stringResource(id = R.string.copy_totp_automatically_description), + supportingText = stringResource(id = R.string.copy_totp_automatically_description), isChecked = state.isCopyTotpAutomaticallyEnabled, onCheckedChange = remember(viewModel) { { viewModel.trySendAction(AutoFillAction.CopyTotpAutomaticallyClick(it)) } }, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("CopyTotpAutomaticallySwitch") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(8.dp)) BitwardenSwitch( label = stringResource(id = R.string.ask_to_add_login), - description = stringResource(id = R.string.ask_to_add_login_description), + supportingText = stringResource(id = R.string.ask_to_add_login_description), isChecked = state.isAskToAddLoginEnabled, onCheckedChange = remember(viewModel) { { viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(it)) } }, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("AskToAddLoginSwitch") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(8.dp)) DefaultUriMatchTypeRow( selectedUriMatchType = state.defaultUriMatchType, onUriMatchTypeSelect = remember(viewModel) { @@ -252,8 +271,10 @@ fun AutoFillScreen( }, modifier = Modifier .testTag("DefaultUriMatchDetectionChooser") + .standardHorizontalMargin() .fillMaxWidth(), ) + Spacer(modifier = Modifier.height(8.dp)) BitwardenTextRow( text = stringResource(id = R.string.block_auto_fill), description = stringResource( @@ -262,7 +283,10 @@ fun AutoFillScreen( onClick = remember(viewModel) { { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) } }, - modifier = Modifier.fillMaxWidth(), + cardStyle = CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), ) Spacer(modifier = Modifier.height(16.dp)) } @@ -278,7 +302,7 @@ private fun AccessibilityAutofillSwitch( var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) } BitwardenSwitch( label = stringResource(id = R.string.accessibility), - description = stringResource(id = R.string.accessibility_description5), + supportingText = stringResource(id = R.string.accessibility_description5), isChecked = isAccessibilityAutoFillEnabled, onCheckedChange = { if (isAccessibilityAutoFillEnabled) { @@ -287,6 +311,7 @@ private fun AccessibilityAutofillSwitch( shouldShowDialog = true } }, + cardStyle = CardStyle.Full, modifier = modifier.testTag(tag = "AccessibilityAutofillSwitch"), ) @@ -318,6 +343,7 @@ private fun DefaultUriMatchTypeRow( text = stringResource(id = R.string.default_uri_match_detection), description = stringResource(id = R.string.default_uri_match_detection_description), onClick = { shouldShowDialog = true }, + cardStyle = CardStyle.Full, modifier = modifier, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/AddEditBlockedUriDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/AddEditBlockedUriDialog.kt index 01c477e44e..8305af8388 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/AddEditBlockedUriDialog.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/AddEditBlockedUriDialog.kt @@ -21,10 +21,12 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -82,15 +84,16 @@ fun AddEditBlockedUriDialog( BitwardenTextField( label = stringResource(id = R.string.enter_uri), isError = errorMessage != null, - hint = errorMessage ?: stringResource( + supportingText = errorMessage ?: stringResource( id = R.string.format_x_separate_multiple_ur_is_with_a_comma, "http://domain.com", ), value = uri, onValueChange = onUriChange, keyboardType = KeyboardType.Uri, + cardStyle = CardStyle.Full, modifier = Modifier - .padding(start = 16.dp, end = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt index 2b7e767188..933d90d7d8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt @@ -249,6 +249,7 @@ private fun BlockAutoFillListItem( ) { Row( modifier = Modifier + .defaultMinSize(minHeight = 60.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple( @@ -257,7 +258,6 @@ private fun BlockAutoFillListItem( onClick = onClick, ) .bottomDivider(paddingStart = 16.dp) - .defaultMinSize(minHeight = 56.dp) .padding(end = 8.dp, top = 16.dp, bottom = 16.dp) .then(modifier), horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt index bb8cc52492..5b0b633dfd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -35,6 +34,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard @@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager @@ -194,13 +195,13 @@ private fun ExportVaultScreenContent( .imePadding() .verticalScroll(rememberScrollState()), ) { - + Spacer(modifier = Modifier.height(height = 12.dp)) if (state.policyPreventsExport) { BitwardenInfoCalloutCard( text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect), modifier = Modifier .testTag("DisablePrivateVaultPolicyLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -219,15 +220,15 @@ private fun ExportVaultScreenContent( onExportFormatOptionSelected(selectedOption) }, isEnabled = !state.policyPreventsExport, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FileFormatPicker") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) - if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) { + Spacer(modifier = Modifier.height(8.dp)) var showPassword by rememberSaveable { mutableStateOf(false) } BitwardenPasswordField( label = stringResource(id = R.string.file_password), @@ -235,20 +236,27 @@ private fun ExportVaultScreenContent( onValueChange = onFilePasswordInputChanged, showPassword = showPassword, showPasswordChange = { showPassword = it }, - hint = stringResource(id = R.string.password_used_to_export), + supportingTextContent = { + PasswordStrengthIndicator( + state = state.passwordStrengthState, + currentCharacterCount = state.passwordInput.length, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = stringResource(id = R.string.password_used_to_export), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FilePasswordEntry") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) - PasswordStrengthIndicator( - modifier = Modifier.padding(horizontal = 16.dp), - state = state.passwordStrengthState, - currentCharacterCount = state.passwordInput.length, - ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordField( label = stringResource(id = R.string.confirm_file_password), @@ -256,16 +264,15 @@ private fun ExportVaultScreenContent( onValueChange = onConfirmFilePasswordInputChanged, showPassword = showPassword, showPasswordChange = { showPassword = it }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("ConfirmFilePasswordEntry") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) } if (state.showSendCodeButton) { - Spacer(modifier = Modifier.height(16.dp)) Text( @@ -274,7 +281,7 @@ private fun ExportVaultScreenContent( style = BitwardenTheme.typography.bodyMedium, color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -286,7 +293,7 @@ private fun ExportVaultScreenContent( isEnabled = !state.policyPreventsExport, modifier = Modifier .testTag("SendTOTPCodeButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -296,49 +303,40 @@ private fun ExportVaultScreenContent( label = stringResource(id = R.string.verification_code), value = state.passwordInput, readOnly = state.policyPreventsExport, - hint = stringResource(id = R.string.confirm_your_identity), + supportingText = stringResource(id = R.string.confirm_your_identity), onValueChange = onPasswordInputChanged, keyboardType = KeyboardType.Number, + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - - Spacer(modifier = Modifier.height(16.dp)) } else { + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenPasswordField( label = stringResource(id = R.string.master_password), + supportingText = stringResource( + id = R.string.export_vault_master_password_description, + ), value = state.passwordInput, readOnly = state.policyPreventsExport, onValueChange = onPasswordInputChanged, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("MasterPasswordEntry") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.export_vault_master_password_description), - textAlign = TextAlign.Start, - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(8.dp)) } + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenOutlinedButton( label = stringResource(id = R.string.export_vault), onClick = onExportVaultClick, isEnabled = !state.policyPreventsExport, modifier = Modifier .testTag("ExportVaultButton") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt index 867df5b3fe..17158a3d77 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt @@ -1,22 +1,19 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders import android.widget.Toast -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize +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.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -31,11 +28,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect -import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.fab.BitwardenFloatingActionButton +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem @@ -150,33 +149,24 @@ private fun FoldersContent( LazyColumn( modifier = modifier, ) { - items(foldersList) { - Row( + item { + Spacer(modifier = Modifier.height(height = 12.dp)) + } + itemsIndexed(foldersList) { index, it -> + BitwardenTextRow( + text = it.name, + onClick = { onItemClick(it.id) }, + textTestTag = "FolderName", + cardStyle = foldersList.toListItemCardStyle(index = index), modifier = Modifier - .testTag("FolderCell") - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = { onItemClick(it.id) }, - ) - .bottomDivider(paddingStart = 16.dp) - .defaultMinSize(minHeight = 56.dp) - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .testTag("FolderName") - .padding(start = 16.dp) - .weight(1f), - text = it.name, - style = BitwardenTheme.typography.bodyLarge, - color = BitwardenTheme.colorScheme.text.primary, - ) - } + .fillMaxWidth() + .standardHorizontalMargin() + .testTag(tag = "FolderCell"), + ) + } + item { + Spacer(modifier = Modifier.height(height = 88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt index 4fcdace2df..c04691fa17 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt @@ -2,9 +2,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit import android.widget.Toast import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -24,6 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData @@ -34,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -127,6 +131,7 @@ fun FolderAddEditScreen( Column( modifier = Modifier.fillMaxSize(), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = viewState.folderName, @@ -134,10 +139,12 @@ fun FolderAddEditScreen( { viewModel.trySendAction(FolderAddEditAction.NameTextChange(it)) } }, textFieldTestTag = "FolderNameField", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) + Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt index acff0b3db4..6cd7fdd80d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt @@ -8,6 +8,7 @@ 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -33,6 +34,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency import com.x8bit.bitwarden.data.platform.repository.util.displayLabel import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog @@ -40,6 +42,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -102,20 +105,22 @@ fun OtherScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenSwitch( label = stringResource(id = R.string.enable_sync_on_refresh), - description = stringResource(id = R.string.enable_sync_on_refresh_description), + supportingText = stringResource(id = R.string.enable_sync_on_refresh_description), isChecked = state.allowSyncOnRefresh, onCheckedChange = remember(viewModel) { { viewModel.trySendAction(OtherAction.AllowSyncToggle(it)) } }, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("SyncOnRefreshSwitch") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenOutlinedButton( onClick = remember(viewModel) { @@ -125,10 +130,10 @@ fun OtherScreen( modifier = Modifier .fillMaxWidth() .testTag("SyncNowButton") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) Row( modifier = Modifier @@ -151,7 +156,7 @@ fun OtherScreen( ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) ClearClipboardFrequencyRow( currentSelection = state.clearClipboardFrequency, @@ -160,9 +165,12 @@ fun OtherScreen( }, modifier = Modifier .testTag("ClearClipboardChooser") - .fillMaxWidth(), + .fillMaxWidth() + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) + ScreenCaptureRow( currentValue = state.allowScreenCapture, onValueChange = remember(viewModel) { @@ -171,8 +179,10 @@ fun OtherScreen( modifier = Modifier .fillMaxWidth() .testTag("AllowScreenCaptureSwitch") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) + + Spacer(modifier = Modifier.navigationBarsPadding()) } } } @@ -195,6 +205,7 @@ private fun ScreenCaptureRow( shouldShowScreenCaptureConfirmDialog = true } }, + cardStyle = CardStyle.Full, modifier = modifier, ) @@ -226,6 +237,7 @@ private fun ClearClipboardFrequencyRow( text = stringResource(id = R.string.clear_clipboard), description = stringResource(id = R.string.clear_clipboard_description), onClick = { shouldShowClearClipboardDialog = true }, + cardStyle = CardStyle.Full, modifier = modifier, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt index 7a5b4a5249..8fb8ae7275 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt @@ -3,8 +3,10 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +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.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -30,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold @@ -106,6 +109,7 @@ fun VaultSettingsScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(12.dp)) AnimatedVisibility( visible = state.shouldShowImportCard, label = "ImportLoginsActionCard", @@ -132,7 +136,7 @@ fun VaultSettingsScreen( }, modifier = Modifier .standardHorizontalMargin() - .padding(top = 12.dp, bottom = 16.dp), + .padding(bottom = 16.dp), ) } BitwardenTextRow( @@ -140,9 +144,11 @@ fun VaultSettingsScreen( onClick = remember(viewModel) { { viewModel.trySendAction(VaultSettingsAction.FoldersButtonClick) } }, - withDivider = true, + withDivider = false, + cardStyle = CardStyle.Top(), modifier = Modifier .testTag("FoldersLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -151,9 +157,11 @@ fun VaultSettingsScreen( onClick = remember(viewModel) { { viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) } }, - withDivider = true, + withDivider = false, + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("ExportVaultLabel") + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -163,9 +171,11 @@ fun VaultSettingsScreen( onClick = remember(viewModel) { { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) } }, - withDivider = true, + withDivider = false, + cardStyle = CardStyle.Bottom, modifier = Modifier .testTag("ImportItemsLinkItemView") + .standardHorizontalMargin() .fillMaxWidth(), ) } else { @@ -174,15 +184,16 @@ fun VaultSettingsScreen( onConfirmClick = remember(viewModel) { { viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) } }, - withDivider = true, + withDivider = false, dialogTitle = stringResource(id = R.string.continue_to_web_app), - dialogMessage = - stringResource( + dialogMessage = stringResource( id = R.string.you_can_import_data_to_your_vault_on_x, state.importUrl, ), + cardStyle = CardStyle.Bottom, modifier = Modifier .testTag("ImportItemsLinkItemView") + .standardHorizontalMargin() .fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt index 97d0a1e8e8..0bcb0251dc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/BitwardenShapes.kt @@ -13,11 +13,13 @@ data class BitwardenShapes( val coachmark: CornerBasedShape, val content: CornerBasedShape, val contentBottom: CornerBasedShape, + val contentMiddle: CornerBasedShape, val contentTop: CornerBasedShape, val dialog: CornerBasedShape, val fab: CornerBasedShape, val infoCallout: CornerBasedShape, val menu: CornerBasedShape, + val progressIndicator: CornerBasedShape, val segmentedControl: CornerBasedShape, val snackbar: CornerBasedShape, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt index adf32c8c2f..28f2e40d08 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/shape/Shapes.kt @@ -13,11 +13,13 @@ val bitwardenShapes: BitwardenShapes = BitwardenShapes( coachmark = RoundedCornerShape(size = 8.dp), content = RoundedCornerShape(size = 8.dp), contentBottom = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp), + contentMiddle = RoundedCornerShape(0.dp), contentTop = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), dialog = RoundedCornerShape(size = 28.dp), fab = CircleShape, infoCallout = RoundedCornerShape(size = 8.dp), menu = RoundedCornerShape(size = 4.dp), + progressIndicator = CircleShape, segmentedControl = CircleShape, snackbar = RoundedCornerShape(size = 8.dp), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index da2052a5b7..08293c8fd6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -8,7 +8,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -35,19 +36,20 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions -import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType import com.x8bit.bitwarden.ui.platform.components.model.TooltipData import com.x8bit.bitwarden.ui.platform.components.model.TopAppBarDividerStyle @@ -301,6 +303,7 @@ private fun ScrollContent( Column( modifier = modifier .fillMaxHeight() + .imePadding() .verticalScroll(rememberScrollState()), ) { Spacer(modifier = Modifier.height(12.dp)) @@ -309,7 +312,7 @@ private fun ScrollContent( text = stringResource(id = R.string.password_generator_policy_in_effect), modifier = Modifier .testTag("PasswordGeneratorPolicyInEffectLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -320,7 +323,7 @@ private fun ScrollContent( generatedText = state.generatedText, onRegenerateClick = onRegenerateClick, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) @@ -332,20 +335,11 @@ private fun ScrollContent( modifier = Modifier .testTag(tag = "CopyValueButton") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(24.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.options), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - - Spacer(modifier = Modifier.height(8.dp)) - when (val selectedType = state.selectedType) { is GeneratorState.MainType.Passphrase -> { PassphraseTypeContent( @@ -373,6 +367,8 @@ private fun ScrollContent( ) } } + Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -388,7 +384,7 @@ private fun GeneratedStringItem( value = generatedText, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_generate, contentDescription = stringResource(id = R.string.generate_password), onClick = onRegenerateClick, @@ -402,6 +398,7 @@ private fun GeneratedStringItem( visualTransformation = nonLetterColorVisualTransformation(), modifier = modifier, textToolbarType = TextToolbarType.NONE, + cardStyle = CardStyle.Full, ) } @@ -459,8 +456,6 @@ private fun ColumnScope.PasswordTypeContent( passwordTypeState: GeneratorState.MainType.Password, passwordHandlers: PasswordHandlers, ) { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenSlider( value = passwordTypeState.length, onValueChange = { newValue, isUserInteracting -> @@ -471,41 +466,58 @@ private fun ColumnScope.PasswordTypeContent( range = passwordTypeState.minLength..passwordTypeState.maxLength, sliderTag = "PasswordLengthSlider", valueTag = "PasswordLengthLabel", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(end = 28.dp), + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - ) { - - PasswordCapitalLettersToggleItem( - useCapitals = passwordTypeState.useCapitals, - onPasswordToggleCapitalLettersChange = passwordHandlers - .onPasswordToggleCapitalLettersChange, - enabled = passwordTypeState.capitalsEnabled, - ) - PasswordLowercaseLettersToggleItem( - useLowercase = passwordTypeState.useLowercase, - onPasswordToggleLowercaseLettersChange = passwordHandlers - .onPasswordToggleLowercaseLettersChange, - enabled = passwordTypeState.lowercaseEnabled, - ) - PasswordNumbersToggleItem( - useNumbers = passwordTypeState.useNumbers, - onPasswordToggleNumbersChange = passwordHandlers.onPasswordToggleNumbersChange, - enabled = passwordTypeState.numbersEnabled, - ) - PasswordSpecialCharactersToggleItem( - useSpecialChars = passwordTypeState.useSpecialChars, - onPasswordToggleSpecialCharactersChange = passwordHandlers - .onPasswordToggleSpecialCharactersChange, - enabled = passwordTypeState.specialCharsEnabled, - ) - } + PasswordCapitalLettersToggleItem( + useCapitals = passwordTypeState.useCapitals, + onPasswordToggleCapitalLettersChange = passwordHandlers + .onPasswordToggleCapitalLettersChange, + enabled = passwordTypeState.capitalsEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + PasswordLowercaseLettersToggleItem( + useLowercase = passwordTypeState.useLowercase, + onPasswordToggleLowercaseLettersChange = passwordHandlers + .onPasswordToggleLowercaseLettersChange, + enabled = passwordTypeState.lowercaseEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + PasswordNumbersToggleItem( + useNumbers = passwordTypeState.useNumbers, + onPasswordToggleNumbersChange = passwordHandlers.onPasswordToggleNumbersChange, + enabled = passwordTypeState.numbersEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + PasswordSpecialCharactersToggleItem( + useSpecialChars = passwordTypeState.useSpecialChars, + onPasswordToggleSpecialCharactersChange = passwordHandlers + .onPasswordToggleSpecialCharactersChange, + enabled = passwordTypeState.specialCharsEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + PasswordAvoidAmbiguousCharsToggleItem( + avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars, + onPasswordToggleAvoidAmbiguousCharsChange = passwordHandlers + .onPasswordToggleAvoidAmbiguousCharsChange, + enabled = passwordTypeState.ambiguousCharsEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) Spacer(modifier = Modifier.height(8.dp)) @@ -514,25 +526,20 @@ private fun ColumnScope.PasswordTypeContent( onPasswordMinNumbersCounterChange = passwordHandlers.onPasswordMinNumbersCounterChange, maxValue = max(passwordTypeState.maxNumbersAllowed, passwordTypeState.minNumbersAllowed), minValue = passwordTypeState.minNumbersAllowed, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(8.dp)) - PasswordMinSpecialCharactersCounterItem( minSpecial = passwordTypeState.minSpecial, onPasswordMinSpecialCharactersChange = passwordHandlers .onPasswordMinSpecialCharactersChange, maxValue = max(passwordTypeState.maxSpecialAllowed, passwordTypeState.minSpecialAllowed), minValue = passwordTypeState.minSpecialAllowed, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - PasswordAvoidAmbiguousCharsToggleItem( - avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars, - onPasswordToggleAvoidAmbiguousCharsChange = passwordHandlers - .onPasswordToggleAvoidAmbiguousCharsChange, - enabled = passwordTypeState.ambiguousCharsEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -549,10 +556,8 @@ private fun PasswordCapitalLettersToggleItem( isChecked = useCapitals, onCheckedChange = onPasswordToggleCapitalLettersChange, enabled = enabled, - modifier = modifier - .fillMaxWidth() - .testTag("UppercaseAtoZToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Top(), + modifier = modifier.testTag(tag = "UppercaseAtoZToggle"), ) } @@ -569,10 +574,8 @@ private fun PasswordLowercaseLettersToggleItem( isChecked = useLowercase, onCheckedChange = onPasswordToggleLowercaseLettersChange, enabled = enabled, - modifier = modifier - .fillMaxWidth() - .testTag("LowercaseAtoZToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Middle(), + modifier = modifier.testTag(tag = "LowercaseAtoZToggle"), ) } @@ -589,10 +592,8 @@ private fun PasswordNumbersToggleItem( isChecked = useNumbers, onCheckedChange = onPasswordToggleNumbersChange, enabled = enabled, - modifier = modifier - .fillMaxWidth() - .testTag("NumbersZeroToNineToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Middle(), + modifier = modifier.testTag(tag = "NumbersZeroToNineToggle"), ) } @@ -609,10 +610,8 @@ private fun PasswordSpecialCharactersToggleItem( isChecked = useSpecialChars, onCheckedChange = onPasswordToggleSpecialCharactersChange, enabled = enabled, - modifier = modifier - .fillMaxWidth() - .testTag("SpecialCharactersToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Middle(), + modifier = modifier.testTag(tag = "SpecialCharactersToggle"), ) } @@ -629,9 +628,8 @@ private fun PasswordMinNumbersCounterItem( value = minNumbers.coerceIn(minValue, maxValue), range = minValue..maxValue, onValueChange = onPasswordMinNumbersCounterChange, - modifier = modifier - .testTag("MinNumberValueLabel") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Top(), + modifier = modifier.testTag(tag = "MinNumberValueLabel"), ) } @@ -648,9 +646,8 @@ private fun PasswordMinSpecialCharactersCounterItem( value = minSpecial.coerceIn(minValue, maxValue), range = minValue..maxValue, onValueChange = onPasswordMinSpecialCharactersChange, - modifier = modifier - .testTag("MinSpecialValueLabel") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Bottom, + modifier = modifier.testTag(tag = "MinSpecialValueLabel"), ) } @@ -666,10 +663,8 @@ private fun PasswordAvoidAmbiguousCharsToggleItem( isChecked = avoidAmbiguousChars, enabled = enabled, onCheckedChange = onPasswordToggleAvoidAmbiguousCharsChange, - modifier = modifier - .fillMaxWidth() - .testTag("AvoidAmbiguousCharsToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Bottom, + modifier = modifier.testTag(tag = "AvoidAmbiguousCharsToggle"), ) } @@ -682,13 +677,14 @@ private fun ColumnScope.PassphraseTypeContent( passphraseTypeState: GeneratorState.MainType.Passphrase, passphraseHandlers: PassphraseHandlers, ) { - Spacer(modifier = Modifier.height(8.dp)) - PassphraseNumWordsCounterItem( numWords = passphraseTypeState.numWords, onPassphraseNumWordsCounterChange = passphraseHandlers.onPassphraseNumWordsCounterChange, minValue = passphraseTypeState.minNumWords, maxValue = passphraseTypeState.maxNumWords, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(8.dp)) @@ -696,26 +692,31 @@ private fun ColumnScope.PassphraseTypeContent( PassphraseWordSeparatorInputItem( wordSeparator = passphraseTypeState.wordSeparator, onPassphraseWordSeparatorChange = passphraseHandlers.onPassphraseWordSeparatorChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - ) { - PassphraseCapitalizeToggleItem( - capitalize = passphraseTypeState.capitalize, - onPassphraseCapitalizeToggleChange = passphraseHandlers - .onPassphraseCapitalizeToggleChange, - enabled = passphraseTypeState.capitalizeEnabled, - ) - PassphraseIncludeNumberToggleItem( - includeNumber = passphraseTypeState.includeNumber, - onPassphraseIncludeNumberToggleChange = passphraseHandlers - .onPassphraseIncludeNumberToggleChange, - enabled = passphraseTypeState.includeNumberEnabled, - ) - } + PassphraseCapitalizeToggleItem( + capitalize = passphraseTypeState.capitalize, + onPassphraseCapitalizeToggleChange = passphraseHandlers + .onPassphraseCapitalizeToggleChange, + enabled = passphraseTypeState.capitalizeEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + PassphraseIncludeNumberToggleItem( + includeNumber = passphraseTypeState.includeNumber, + onPassphraseIncludeNumberToggleChange = passphraseHandlers + .onPassphraseIncludeNumberToggleChange, + enabled = passphraseTypeState.includeNumberEnabled, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) } @Composable @@ -726,17 +727,14 @@ private fun PassphraseNumWordsCounterItem( minValue: Int = PASSPHRASE_MIN_NUMBER_OF_WORDS, maxValue: Int = PASSPHRASE_MAX_NUMBER_OF_WORDS, ) { - val coercedNumWords = numWords.coerceIn(minValue, maxValue) - BitwardenStepper( label = stringResource(id = R.string.number_of_words), - value = coercedNumWords, + value = numWords.coerceIn(minimumValue = minValue, maximumValue = maxValue), range = minValue..maxValue, onValueChange = onPassphraseNumWordsCounterChange, stepperActionsTestTag = "NumberOfWordsStepper", - modifier = modifier - .testTag("NumberOfWordsLabel") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Full, + modifier = modifier.testTag(tag = "NumberOfWordsLabel"), ) } @@ -758,10 +756,9 @@ private fun PassphraseWordSeparatorInputItem( onPassphraseWordSeparatorChange(char) } }, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Full, textFieldTestTag = "WordSeparatorEntry", + modifier = modifier, ) } @@ -777,10 +774,8 @@ private fun PassphraseCapitalizeToggleItem( isChecked = capitalize, onCheckedChange = onPassphraseCapitalizeToggleChange, enabled = enabled, - modifier = modifier - .testTag("CapitalizePassphraseToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Top(), + modifier = modifier.testTag(tag = "CapitalizePassphraseToggle"), ) } @@ -796,10 +791,8 @@ private fun PassphraseIncludeNumberToggleItem( isChecked = includeNumber, enabled = enabled, onCheckedChange = onPassphraseIncludeNumberToggleChange, - modifier = modifier - .testTag("IncludeNumbersToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Bottom, + modifier = modifier.testTag(tag = "IncludeNumbersToggle"), ) } @@ -817,7 +810,14 @@ private fun ColumnScope.UsernameTypeItems( catchAllEmailHandlers: CatchAllEmailHandlers, randomWordHandlers: RandomWordHandlers, ) { - UsernameOptionsItem(usernameState, onSubStateOptionClicked, usernameTypeHandlers) + UsernameOptionsItem( + currentSubState = usernameState, + onSubStateOptionClicked = onSubStateOptionClicked, + usernameTypeHandlers = usernameTypeHandlers, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) when (val selectedType = usernameState.selectedType) { is GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail -> { @@ -876,10 +876,8 @@ private fun UsernameOptionsItem( onClick = usernameTypeHandlers.onUsernameTooltipClicked, contentDescription = stringResource(id = R.string.learn_more), ), - modifier = modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .testTag("UsernameTypePicker"), + cardStyle = CardStyle.Full, + modifier = modifier.testTag(tag = "UsernameTypePicker"), ) } @@ -898,20 +896,23 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( ServiceTypeOptionsItem( currentSubState = usernameTypeState, onSubStateOptionClicked = forwardedEmailAliasHandlers.onServiceChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(8.dp)) when (usernameTypeState.selectedServiceType) { - is ServiceType.AddyIo -> { BitwardenPasswordField( label = stringResource(id = R.string.api_access_token), value = usernameTypeState.selectedServiceType.apiAccessToken, onValueChange = forwardedEmailAliasHandlers.onAddyIoAccessTokenTextChange, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -922,10 +923,11 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( label = stringResource(id = R.string.domain_name_required_parenthesis), value = usernameTypeState.selectedServiceType.domainName, onValueChange = forwardedEmailAliasHandlers.onAddyIoDomainNameTextChange, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), textFieldTestTag = "AnonAddyDomainNameEntry", + cardStyle = CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), ) } @@ -935,8 +937,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( value = usernameTypeState.selectedServiceType.apiKey, onValueChange = forwardedEmailAliasHandlers.onDuckDuckGoApiKeyTextChange, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -948,8 +951,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( value = usernameTypeState.selectedServiceType.apiKey, onValueChange = forwardedEmailAliasHandlers.onFastMailApiKeyTextChange, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -961,8 +965,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( value = usernameTypeState.selectedServiceType.apiAccessToken, onValueChange = forwardedEmailAliasHandlers.onFirefoxRelayAccessTokenTextChange, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -974,8 +979,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( value = usernameTypeState.selectedServiceType.apiKey, onValueChange = forwardedEmailAliasHandlers.onForwardEmailApiKeyTextChange, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -986,10 +992,11 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( label = stringResource(id = R.string.domain_name_required_parenthesis), value = usernameTypeState.selectedServiceType.domainName, onValueChange = forwardedEmailAliasHandlers.onForwardEmailDomainNameTextChange, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), textFieldTestTag = "ForwardedEmailDomainNameEntry", + cardStyle = CardStyle.Full, + modifier = Modifier + .standardHorizontalMargin() + .fillMaxWidth(), ) } @@ -999,8 +1006,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( value = usernameTypeState.selectedServiceType.apiKey, onValueChange = forwardedEmailAliasHandlers.onSimpleLoginApiKeyTextChange, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -1013,8 +1021,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( value = obfuscatedTextField, onValueChange = { obfuscatedTextField = it }, showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + cardStyle = CardStyle.Full, modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("ForwardedEmailApiSecretEntry") .fillMaxWidth(), ) @@ -1042,10 +1051,8 @@ private fun ServiceTypeOptionsItem( optionsWithStrings.entries.first { it.value == selectedOption }.key onSubStateOptionClicked(selectedOptionId) }, - modifier = modifier - .padding(horizontal = 16.dp) - .testTag("ServiceTypePicker") - .fillMaxWidth(), + cardStyle = CardStyle.Full, + modifier = modifier.testTag(tag = "ServiceTypePicker"), ) } @@ -1061,6 +1068,9 @@ private fun ColumnScope.PlusAddressedEmailTypeContent( PlusAddressedEmailTextInputItem( email = usernameTypeState.email, onPlusAddressedEmailTextChange = plusAddressedEmailHandlers.onEmailChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -1074,10 +1084,9 @@ private fun PlusAddressedEmailTextInputItem( label = stringResource(id = R.string.email_required_parenthesis), value = email, onValueChange = onPlusAddressedEmailTextChange, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), textFieldTestTag = "PlusAddressedEmailEntry", + cardStyle = CardStyle.Full, + modifier = modifier, ) } @@ -1095,6 +1104,9 @@ private fun ColumnScope.CatchAllEmailTypeContent( CatchAllEmailTextInputItem( domain = usernameTypeState.domainName, onDomainTextChange = catchAllEmailHandlers.onDomainChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -1108,10 +1120,9 @@ private fun CatchAllEmailTextInputItem( label = stringResource(id = R.string.domain_name_required_parenthesis), value = domain, onValueChange = onDomainTextChange, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), textFieldTestTag = "CatchAllEmailDomainEntry", + cardStyle = CardStyle.Full, + modifier = modifier, ) } @@ -1124,16 +1135,22 @@ private fun ColumnScope.RandomWordTypeContent( randomWordTypeState: GeneratorState.MainType.Username.UsernameType.RandomWord, randomWordHandlers: RandomWordHandlers, ) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) RandomWordCapitalizeToggleItem( capitalize = randomWordTypeState.capitalize, onRandomWordCapitalizeToggleChange = randomWordHandlers.onCapitalizeChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) RandomWordIncludeNumberToggleItem( includeNumber = randomWordTypeState.includeNumber, onRandomWordIncludeNumberToggleChange = randomWordHandlers.onIncludeNumberChange, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -1147,10 +1164,8 @@ private fun RandomWordCapitalizeToggleItem( label = stringResource(id = R.string.capitalize), isChecked = capitalize, onCheckedChange = onRandomWordCapitalizeToggleChange, - modifier = modifier - .fillMaxWidth() - .testTag("CapitalizeRandomWordUsernameToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Top(), + modifier = modifier.testTag(tag = "CapitalizeRandomWordUsernameToggle"), ) } @@ -1164,10 +1179,8 @@ private fun RandomWordIncludeNumberToggleItem( label = stringResource(id = R.string.include_number), isChecked = includeNumber, onCheckedChange = onRandomWordIncludeNumberToggleChange, - modifier = modifier - .fillMaxWidth() - .testTag("IncludeNumberRandomWordUsernameToggle") - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Bottom, + modifier = modifier.testTag(tag = "IncludeNumberRandomWordUsernameToggle"), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt index 58fe7624d8..e53a78abd4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt @@ -18,9 +18,11 @@ 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.ui.platform.base.util.cardBackground import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth import com.x8bit.bitwarden.ui.platform.base.util.withVisualTransformation import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -37,12 +39,18 @@ fun PasswordHistoryListItem( label: String, supportingLabel: String, onCopyClick: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { Row( - modifier = Modifier - .padding(vertical = 16.dp) - .then(modifier), + modifier = modifier + .cardBackground(cardStyle = cardStyle) + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + end = 4.dp, + ), verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { @@ -90,6 +98,7 @@ private fun PasswordHistoryListItem_preview() { label = "8gr6uY8CLYQwzr#", supportingLabel = "8/24/2023 11:07 AM", onCopyClick = {}, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt index 149639760f..136c7a5d0a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt @@ -7,11 +7,11 @@ import androidx.compose.foundation.layout.Column 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.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -30,10 +30,11 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData -import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.indicator.BitwardenCircularProgressIndicator import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -72,8 +73,8 @@ fun PasswordHistoryScreen( BitwardenTopAppBar( title = stringResource(id = R.string.password_history), scrollBehavior = scrollBehavior, - navigationIcon = rememberVectorPainter(id = R.drawable.ic_close), - navigationIconContentDescription = stringResource(id = R.string.close), + navigationIcon = rememberVectorPainter(id = R.drawable.ic_back), + navigationIconContentDescription = stringResource(id = R.string.back), onNavigationIconClick = remember(viewModel) { { viewModel.trySendAction(PasswordHistoryAction.CloseClick) } }, @@ -155,19 +156,23 @@ private fun PasswordHistoryContent( modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier) { - items(state.passwords) { password -> + item { + Spacer(modifier = Modifier.height(height = 12.dp)) + } + itemsIndexed(state.passwords) { index, password -> PasswordHistoryListItem( label = password.password, supportingLabel = password.date, onCopyClick = { onPasswordCopyClick(password) }, + cardStyle = state.passwords.toListItemCardStyle(index = index), modifier = Modifier .testTag("GeneratedPasswordRow") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - BitwardenHorizontalDivider(modifier = Modifier.fillMaxWidth()) } item { + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt index 36bff13307..793c884a62 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt @@ -6,16 +6,19 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers @@ -36,14 +39,18 @@ fun SendContent( ) { LazyColumn(modifier = modifier) { item { - if (policyDisablesSend) { + Spacer(modifier = Modifier.height(height = 12.dp)) + } + if (policyDisablesSend) { + item { BitwardenInfoCalloutCard( text = stringResource(id = R.string.send_disabled_warning), modifier = Modifier .testTag("SendOptionsPolicyInEffectLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } } @@ -53,8 +60,10 @@ fun SendContent( supportingLabel = SEND_TYPES_COUNT.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { @@ -63,10 +72,11 @@ fun SendContent( supportingLabel = state.textTypeCount.toString(), startIcon = rememberVectorPainter(id = R.drawable.ic_file_text), onClick = sendHandlers.onTextTypeClick, + cardStyle = CardStyle.Top(dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("SendTextFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -76,10 +86,11 @@ fun SendContent( supportingLabel = state.fileTypeCount.toString(), startIcon = rememberVectorPainter(id = R.drawable.ic_file), onClick = sendHandlers.onFileTypeClick, + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() .testTag("SendFileFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -90,11 +101,13 @@ fun SendContent( supportingLabel = state.sendItems.size.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(state.sendItems) { + itemsIndexed(state.sendItems) { index, it -> SendListItem( startIcon = IconData.Local(it.type.iconRes), label = it.name, @@ -111,14 +124,12 @@ fun SendContent( } else { null }, + cardStyle = state + .sendItems + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .testTag("SendCell") - .padding( - start = 16.dp, - // There is some built-in padding to the menu button that makes up - // the visual difference here. - end = 12.dp, - ) + .standardHorizontalMargin() .fillMaxWidth(), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt index 4d8856dc33..f6e18c28e8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconRes import com.x8bit.bitwarden.ui.platform.components.model.IconResource @@ -37,6 +38,7 @@ import kotlinx.collections.immutable.toPersistentList * @param onDeleteClick The lambda to be invoked when the delete option is clicked from the menu. * @param onRemovePasswordClick The lambda to be invoked when the remove password option is clicked * from the menu, if `null` the remove password button is not displayed. + * @param cardStyle Indicates the type of card style to be applied. * @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier. * This allows the caller to specify things like padding, size, etc. */ @@ -54,6 +56,7 @@ fun SendListItem( onShareClick: () -> Unit, onDeleteClick: () -> Unit, onRemovePasswordClick: (() -> Unit)?, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } @@ -98,6 +101,7 @@ fun SendListItem( .filter { showMoreOptions } .toPersistentList(), optionsTestTag = "Options", + cardStyle = cardStyle, modifier = modifier, ) if (shouldShowDeleteConfirmationDialog) { @@ -116,7 +120,7 @@ fun SendListItem( } } -@Preview(showBackground = true) +@Preview @Composable private fun SendListItem_preview() { BitwardenTheme { @@ -132,6 +136,7 @@ private fun SendListItem_preview() { onShareClick = {}, onDeleteClick = {}, onRemovePasswordClick = null, + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt index 65088fdcf3..bd4236aafa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt @@ -8,8 +8,10 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding @@ -21,6 +23,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -34,13 +37,20 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.base.util.concat +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper +import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager @@ -69,12 +79,12 @@ fun AddSendContent( Column( modifier = modifier.verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(height = 12.dp)) if (policyDisablesSend) { - Spacer(modifier = Modifier.height(8.dp)) BitwardenInfoCalloutCard( text = stringResource(id = R.string.send_disabled_warning), modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth() .testTag("SendPolicyInEffectLabel"), ) @@ -86,35 +96,36 @@ fun AddSendContent( text = stringResource(id = R.string.send_options_policy_in_effect), modifier = Modifier .testTag(tag = "SendPolicyInEffectLabel") - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) } BitwardenTextField( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), label = stringResource(id = R.string.name), - hint = stringResource(id = R.string.name_info), + supportingText = stringResource(id = R.string.name_info), readOnly = policyDisablesSend, value = state.common.name, onValueChange = addSendHandlers.onNamChange, textFieldTestTag = "SendNameEntry", + cardStyle = CardStyle.Full, ) - Spacer(modifier = Modifier.height(8.dp)) when (val type = state.selectedType) { is AddSendState.ViewState.Content.SendType.File -> { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.file), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) if (isShared) { Text( text = type.name.orEmpty(), @@ -122,6 +133,7 @@ fun AddSendContent( style = BitwardenTheme.typography.bodyMedium, modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) @@ -131,22 +143,31 @@ fun AddSendContent( style = BitwardenTheme.typography.bodySmall, modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) } else if (isAddMode) { - Text( - modifier = Modifier - .testTag(tag = "SendCurrentFileNameLabel") - .align(Alignment.CenterHorizontally), - text = type.name ?: stringResource(id = R.string.no_file_chosen), - color = BitwardenTheme.colorScheme.text.secondary, - style = BitwardenTheme.typography.bodySmall, - ) - Spacer(modifier = Modifier.height(8.dp)) + type.name?.let { + Text( + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = CardStyle.Full) + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ) + .testTag(tag = "SendCurrentFileNameLabel"), + text = it, + color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(8.dp)) + } BitwardenOutlinedButton( label = stringResource(id = R.string.choose_file), onClick = { - @Suppress("MaxLineLength") if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) { addSendHandlers.onChooseFileClick(true) } else { @@ -158,31 +179,30 @@ fun AddSendContent( modifier = Modifier .testTag(tag = "SendChooseFileButton") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) Text( text = stringResource(id = R.string.max_file_size), color = BitwardenTheme.colorScheme.text.secondary, style = BitwardenTheme.typography.bodySmall, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 32.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.type_file_info), - color = BitwardenTheme.colorScheme.text.secondary, - style = BitwardenTheme.typography.bodySmall, - modifier = Modifier - .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) } else { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin() + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = CardStyle.Full) + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + verticalAlignment = Alignment.CenterVertically, ) { Text( text = type.name.orEmpty(), @@ -193,7 +213,7 @@ fun AddSendContent( Spacer(modifier = Modifier.width(8.dp)) Text( text = type.displaySize.orEmpty(), - color = BitwardenTheme.colorScheme.text.primary, + color = BitwardenTheme.colorScheme.text.secondary, style = BitwardenTheme.typography.bodyMedium, ) } @@ -201,33 +221,35 @@ fun AddSendContent( } is AddSendState.ViewState.Content.SendType.Text -> { + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenTextField( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), label = stringResource(id = R.string.text), - hint = stringResource(id = R.string.type_text_info), + supportingText = stringResource(id = R.string.type_text_info), readOnly = policyDisablesSend, value = type.input, singleLine = false, onValueChange = addSendHandlers.onTextChange, textFieldTestTag = "SendTextContentEntry", + cardStyle = CardStyle.Full, ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( modifier = Modifier .testTag(tag = "SendHideTextByDefaultToggle") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), label = stringResource(id = R.string.hide_text_by_default), isChecked = type.isHideByDefaultChecked, onCheckedChange = addSendHandlers.onIsHideByDefaultToggle, readOnly = policyDisablesSend, + cardStyle = CardStyle.Full, ) } } - Spacer(modifier = Modifier.height(16.dp)) AddSendOptions( state = state, sendRestrictionPolicy = policyDisablesSend, @@ -235,7 +257,7 @@ fun AddSendContent( addSendHandlers = addSendHandlers, ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -271,12 +293,14 @@ private fun AddSendOptions( }, onClick = { isExpanded = !isExpanded }, ) - .padding(16.dp) + .minimumInteractiveComponentSize() + .padding(top = 16.dp, bottom = 8.dp) + .standardHorizontalMargin() .semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringResource(id = R.string.options), + text = stringResource(id = R.string.additional_options), color = BitwardenTheme.colorScheme.text.interaction, style = BitwardenTheme.typography.labelLarge, modifier = Modifier.padding(end = 8.dp), @@ -306,7 +330,7 @@ private fun AddSendOptions( modifier = Modifier .testTag("SendDeletionOptionsPicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), dateFormatPattern = state.common.dateFormatPattern, timeFormatPattern = state.common.timeFormatPattern, currentZonedDateTime = state.common.deletionDate, @@ -318,7 +342,7 @@ private fun AddSendOptions( modifier = Modifier .testTag("SendExpirationOptionsPicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), dateFormatPattern = state.common.dateFormatPattern, timeFormatPattern = state.common.timeFormatPattern, currentZonedDateTime = state.common.expirationDate, @@ -326,174 +350,174 @@ private fun AddSendOptions( isEnabled = !sendRestrictionPolicy, ) } else { - BitwardenListHeaderText( - label = stringResource(id = R.string.deletion_date), + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - AddSendCustomDateChooser( - modifier = Modifier - .testTag("SendCustomDeletionDatePicker") - .fillMaxWidth() - .padding(horizontal = 16.dp), - dateLabel = stringResource(id = R.string.deletion_date), - timeLabel = stringResource(id = R.string.deletion_time), - dateFormatPattern = state.common.dateFormatPattern, - timeFormatPattern = state.common.timeFormatPattern, - currentZonedDateTime = state.common.deletionDate, - isEnabled = !sendRestrictionPolicy, - onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) }, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.deletion_date_info), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - - Spacer(modifier = Modifier.height(16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.expiration_date), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - AddSendCustomDateChooser( - modifier = Modifier - .testTag("SendCustomExpirationDatePicker") - .fillMaxWidth() - .padding(horizontal = 16.dp), - dateLabel = stringResource(id = R.string.expiration_date), - timeLabel = stringResource(id = R.string.expiration_time), - dateFormatPattern = state.common.dateFormatPattern, - timeFormatPattern = state.common.timeFormatPattern, - currentZonedDateTime = state.common.expirationDate, - onDateSelect = addSendHandlers.onExpirationDateChange, - isEnabled = !sendRestrictionPolicy, - ) - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, + .standardHorizontalMargin() + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = CardStyle.Full) + .cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp), ) { - Text( - text = stringResource(id = R.string.expiration_date_info), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(4.dp)) - BitwardenTextButton( - label = stringResource(id = R.string.clear), - onClick = addSendHandlers.onClearExpirationDateClick, - isEnabled = state.common.expirationDate != null && !sendRestrictionPolicy, + AddSendCustomDateChooser( modifier = Modifier - .testTag("SendClearExpirationDateButton") - .wrapContentWidth(), + .testTag("SendCustomDeletionDatePicker") + .fillMaxWidth(), + dateLabel = stringResource(id = R.string.deletion_date), + timeLabel = stringResource(id = R.string.deletion_time), + dateFormatPattern = state.common.dateFormatPattern, + timeFormatPattern = state.common.timeFormatPattern, + currentZonedDateTime = state.common.deletionDate, + isEnabled = !sendRestrictionPolicy, + onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) }, ) + BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) + Text( + text = stringResource(id = R.string.deletion_date_info), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(height = 12.dp)) + } + Spacer(modifier = Modifier.height(height = 8.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = CardStyle.Full) + .cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp), + ) { + AddSendCustomDateChooser( + modifier = Modifier + .testTag("SendCustomExpirationDatePicker") + .fillMaxWidth(), + dateLabel = stringResource(id = R.string.expiration_date), + timeLabel = stringResource(id = R.string.expiration_time), + dateFormatPattern = state.common.dateFormatPattern, + timeFormatPattern = state.common.timeFormatPattern, + currentZonedDateTime = state.common.expirationDate, + onDateSelect = addSendHandlers.onExpirationDateChange, + isEnabled = !sendRestrictionPolicy, + ) + BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) + Row( + modifier = Modifier + .padding(start = 16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.expiration_date_info), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.weight(weight = 1f), + ) + Spacer(modifier = Modifier.width(width = 4.dp)) + BitwardenClickableText( + label = stringResource(id = R.string.clear), + onClick = addSendHandlers.onClearExpirationDateClick, + isEnabled = state.common.expirationDate != null && + !sendRestrictionPolicy, + style = BitwardenTheme.typography.labelLarge, + innerPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp), + modifier = Modifier + .testTag("SendClearExpirationDateButton") + .wrapContentWidth(), + ) + } + Spacer(modifier = Modifier.height(height = 12.dp)) } } Spacer(modifier = Modifier.height(8.dp)) BitwardenStepper( label = stringResource(id = R.string.maximum_access_count), + supportingTextContent = { + Text( + text = stringResource(id = R.string.maximum_access_count_info), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + state.common.currentAccessCount.takeUnless { isAddMode }?.let { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = R.string.current_access_count + .asText() + .concat(": ".asText(), it.toString().asText()) + .invoke(), + style = BitwardenTheme.typography.bodySmall, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + } + }, value = state.common.maxAccessCount, onValueChange = addSendHandlers.onMaxAccessCountChange, isDecrementEnabled = state.common.maxAccessCount != null && !sendRestrictionPolicy, isIncrementEnabled = !sendRestrictionPolicy, range = 0..Int.MAX_VALUE, textFieldReadOnly = sendRestrictionPolicy, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("SendMaxAccessCountEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.maximum_access_count_info), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.secondary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp), - ) - if (!isAddMode) { - state.common.currentAccessCount?.let { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp), - ) { - Text( - text = stringResource(id = R.string.current_access_count) + ":", - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = it.toString(), - style = BitwardenTheme.typography.bodySmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - } - } - Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordField( label = stringResource(id = R.string.new_password), - hint = stringResource(id = R.string.password_info), + supportingText = stringResource(id = R.string.password_info), readOnly = sendRestrictionPolicy, value = state.common.passwordInput, onValueChange = addSendHandlers.onPasswordChange, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("SendNewPasswordEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.notes), - hint = stringResource(id = R.string.notes_info), + supportingText = stringResource(id = R.string.notes_info), readOnly = sendRestrictionPolicy, value = state.common.noteInput, singleLine = false, onValueChange = addSendHandlers.onNoteChange, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( modifier = Modifier .testTag("SendHideEmailSwitch") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), label = stringResource(id = R.string.hide_email), isChecked = state.common.isHideEmailChecked, onCheckedChange = addSendHandlers.onHideEmailToggle, readOnly = sendRestrictionPolicy, enabled = state.common.isHideEmailChecked || state.common.isHideEmailAddressEnabled, + cardStyle = CardStyle.Full, ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( modifier = Modifier .testTag("SendDeactivateSwitch") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), label = stringResource(id = R.string.disable_send), isChecked = state.common.isDeactivateChecked, onCheckedChange = addSendHandlers.onDeactivateSendToggle, readOnly = sendRestrictionPolicy, + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt index 34e87a4c2c..96aedfa7a8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt @@ -2,7 +2,9 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -18,7 +20,11 @@ import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import kotlinx.collections.immutable.toImmutableList import java.time.ZonedDateTime @@ -43,7 +49,10 @@ fun SendDeletionDateChooser( val options = DeletionOptions.entries.associateWith { it.text() } var selectedOption: DeletionOptions by rememberSaveable { mutableStateOf(defaultOption) } Column( - modifier = modifier, + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = CardStyle.Full) + .cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp), ) { BitwardenMultiSelectButton( label = stringResource(id = R.string.deletion_date), @@ -59,11 +68,11 @@ fun SendDeletionDateChooser( ) } }, + insets = PaddingValues(top = 6.dp, bottom = 4.dp), ) - AnimatedVisibility(visible = selectedOption == DeletionOptions.CUSTOM) { Column { - Spacer(modifier = Modifier.height(8.dp)) + BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) AddSendCustomDateChooser( dateLabel = stringResource(id = R.string.deletion_date), timeLabel = stringResource(id = R.string.deletion_time), @@ -75,8 +84,8 @@ fun SendDeletionDateChooser( ) } } - - Spacer(modifier = Modifier.height(4.dp)) + BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) Text( text = stringResource(id = R.string.deletion_date_info), style = BitwardenTheme.typography.bodySmall, @@ -85,6 +94,7 @@ fun SendDeletionDateChooser( .fillMaxWidth() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 12.dp)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt index b3615ee3d7..c6d623edca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt @@ -2,7 +2,9 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -18,7 +20,11 @@ import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import kotlinx.collections.immutable.toImmutableList import java.time.ZonedDateTime @@ -43,7 +49,10 @@ fun SendExpirationDateChooser( val options = ExpirationOptions.entries.associateWith { it.text() } var selectedOption: ExpirationOptions by rememberSaveable { mutableStateOf(defaultOption) } Column( - modifier = modifier, + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = CardStyle.Full) + .cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp), ) { BitwardenMultiSelectButton( label = stringResource(id = R.string.expiration_date), @@ -61,11 +70,11 @@ fun SendExpirationDateChooser( ) } }, + insets = PaddingValues(top = 6.dp, bottom = 4.dp), ) - AnimatedVisibility(visible = selectedOption == ExpirationOptions.CUSTOM) { Column { - Spacer(modifier = Modifier.height(8.dp)) + BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) AddSendCustomDateChooser( dateLabel = stringResource(id = R.string.expiration_date), timeLabel = stringResource(id = R.string.expiration_time), @@ -77,8 +86,8 @@ fun SendExpirationDateChooser( ) } } - - Spacer(modifier = Modifier.height(4.dp)) + BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) Text( text = stringResource(id = R.string.expiration_date_info), style = BitwardenTheme.typography.bodySmall, @@ -87,6 +96,7 @@ fun SendExpirationDateChooser( .fillMaxWidth() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 12.dp)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt index 48be5f009e..392b179fd7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,6 +14,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -30,39 +32,40 @@ fun LazyListScope.collectionItemsSelector( if (isCollectionsTitleVisible) { item { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.collections), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } } if (collectionList?.isNotEmpty() == true) { - items(collectionList) { - Spacer(modifier = Modifier.height(8.dp)) + itemsIndexed(collectionList) { index, it -> BitwardenSwitch( label = it.name, isChecked = it.isSelected, onCheckedChange = { _ -> onCollectionSelect(it) }, + cardStyle = collectionList.toListItemCardStyle(index = index), modifier = Modifier .fillMaxWidth() .testTag("CollectionItemCell") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } else { item { - Spacer(modifier = Modifier.height(8.dp)) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) { Text( text = stringResource(id = R.string.no_collections_to_list), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt index 81c4506613..8718a6ff1e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt @@ -17,11 +17,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector @@ -51,10 +53,11 @@ fun LazyListScope.vaultAddEditCardItems( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = commonHandlers.onNameTextChange, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNameEntry", + .standardHorizontalMargin(), ) } item { @@ -63,14 +66,14 @@ fun LazyListScope.vaultAddEditCardItems( label = stringResource(id = R.string.cardholder_name), value = cardState.cardHolderName, onValueChange = cardHandlers.onCardHolderNameTextChange, + textFieldTestTag = "CardholderNameEntry", + cardStyle = CardStyle.Top(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "CardholderNameEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) var showNumber by rememberSaveable { mutableStateOf(value = false) } BitwardenPasswordField( label = stringResource(id = R.string.number), @@ -81,16 +84,16 @@ fun LazyListScope.vaultAddEditCardItems( showNumber = !showNumber cardHandlers.onNumberVisibilityChange(showNumber) }, + showPasswordTestTag = "ShowCardNumberButton", + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("CardNumberEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), - showPasswordTestTag = "ShowCardNumberButton", + .standardHorizontalMargin(), ) } item { val resources = LocalContext.current.resources - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.brand), options = VaultCardBrand @@ -105,14 +108,15 @@ fun LazyListScope.vaultAddEditCardItems( .first { it.longName.toString(resources) == selectedString }, ) }, + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("CardBrandPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { val resources = LocalContext.current.resources - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.expiration_month), options = VaultCardExpirationMonth @@ -127,26 +131,27 @@ fun LazyListScope.vaultAddEditCardItems( .first { it.value.toString(resources) == selectedString }, ) }, + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("CardExpirationMonthPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.expiration_year), value = cardState.expirationYear, onValueChange = cardHandlers.onExpirationYearTextChange, keyboardType = KeyboardType.Number, + textFieldTestTag = "CardExpirationYearEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "CardExpirationYearEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) var showSecurityCode by rememberSaveable { mutableStateOf(value = false) } BitwardenPasswordField( label = stringResource(id = R.string.security_code), @@ -158,24 +163,26 @@ fun LazyListScope.vaultAddEditCardItems( cardHandlers.onSecurityCodeVisibilityChange(showSecurityCode) }, keyboardType = KeyboardType.NumberPassword, + showPasswordTestTag = "CardShowSecurityCodeButton", + cardStyle = CardStyle.Bottom, modifier = Modifier .testTag("CardSecurityCodeEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), - showPasswordTestTag = "CardShowSecurityCodeButton", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.miscellaneous), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), options = commonState @@ -190,37 +197,39 @@ fun LazyListScope.vaultAddEditCardItems( .first { it.name == selectedFolderName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FolderPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource( id = R.string.favorite, ), isChecked = commonState.favorite, onCheckedChange = commonHandlers.onToggleFavorite, + cardStyle = if (commonState.isUnlockWithPasswordEnabled) { + CardStyle.Top() + } else { + CardStyle.Full + }, modifier = Modifier .testTag("ItemFavoriteToggle") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.isUnlockWithPasswordEnabled) { item { - Spacer(modifier = Modifier.height(16.dp)) BitwardenSwitch( label = stringResource(id = R.string.password_prompt), isChecked = commonState.masterPasswordReprompt, onCheckedChange = commonHandlers.onToggleMasterPasswordReprompt, - modifier = Modifier - .testTag("MasterPasswordRepromptToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), actions = { BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_question_circle, @@ -231,53 +240,58 @@ fun LazyListScope.vaultAddEditCardItems( contentColor = BitwardenTheme.colorScheme.icon.secondary, ) }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("MasterPasswordRepromptToggle") + .fillMaxWidth() + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( singleLine = false, label = stringResource(id = R.string.notes), value = commonState.notes, onValueChange = commonHandlers.onNotesTextChange, + textFieldTestTag = "ItemNotesEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNotesEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.custom_fields), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) } items(commonState.customFieldData) { customItem -> - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) VaultAddEditCustomField( customField = customItem, onCustomFieldValueChange = commonHandlers.onCustomFieldValueChange, onCustomFieldAction = commonHandlers.onCustomFieldActionSelect, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), supportedLinkedTypes = persistentListOf( VaultLinkedFieldType.CARDHOLDER_NAME, VaultLinkedFieldType.EXPIRATION_MONTH, @@ -287,6 +301,10 @@ fun LazyListScope.vaultAddEditCardItems( VaultLinkedFieldType.NUMBER, ), onHiddenVisibilityChanged = commonHandlers.onHiddenFieldVisibilityChange, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -296,23 +314,24 @@ fun LazyListScope.vaultAddEditCardItems( onFinishNamingClick = commonHandlers.onAddNewCustomFieldClick, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (isAddItemMode && commonState.hasOrganizations) { item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.ownership), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), options = commonState @@ -327,10 +346,11 @@ fun LazyListScope.vaultAddEditCardItems( .first { it.name == selectedOwnerName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("ItemOwnershipPicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.selectedOwnerId != null) { @@ -340,7 +360,4 @@ fun LazyListScope.vaultAddEditCardItems( ) } } - item { - Spacer(modifier = Modifier.height(24.dp)) - } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt index a8fa78e37c..1eb72c94c4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt @@ -1,7 +1,12 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -11,16 +16,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTextEntryDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import kotlinx.collections.immutable.ImmutableList @@ -33,9 +43,10 @@ import kotlinx.collections.immutable.toImmutableList * @param customField The field that is to be displayed. * @param onCustomFieldValueChange Invoked when the user changes the value. * @param onCustomFieldAction Invoked when the user chooses an action. + * @param onHiddenVisibilityChanged Emits when the visibility of a hidden custom field changes. + * @param cardStyle Indicates the type of card style to be applied. * @param modifier Modifier for the UI elements. * @param supportedLinkedTypes The supported linked types for the vault item. - * @param onHiddenVisibilityChanged Emits when the visibility of a hidden custom field changes. */ @Composable @Suppress("LongMethod") @@ -43,9 +54,10 @@ fun VaultAddEditCustomField( customField: VaultAddEditState.Custom, onCustomFieldValueChange: (VaultAddEditState.Custom) -> Unit, onCustomFieldAction: (CustomFieldAction, VaultAddEditState.Custom) -> Unit, + onHiddenVisibilityChanged: (Boolean) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, supportedLinkedTypes: ImmutableList = persistentListOf(), - onHiddenVisibilityChanged: (Boolean) -> Unit, ) { var shouldShowChooserDialog by remember { mutableStateOf(false) } var shouldShowEditDialog by remember { mutableStateOf(false) } @@ -85,6 +97,7 @@ fun VaultAddEditCustomField( value = customField.value, onValueChanged = { onCustomFieldValueChange(customField.copy(value = it)) }, onEditValue = { shouldShowChooserDialog = true }, + cardStyle = cardStyle, modifier = modifier, ) } @@ -98,6 +111,7 @@ fun VaultAddEditCustomField( }, onVisibilityChanged = onHiddenVisibilityChanged, onEditValue = { shouldShowChooserDialog = true }, + cardStyle = cardStyle, modifier = modifier, ) } @@ -112,6 +126,7 @@ fun VaultAddEditCustomField( onCustomFieldValueChange(customField.copy(vaultLinkedFieldType = it)) }, onEditValue = { shouldShowChooserDialog = true }, + cardStyle = cardStyle, modifier = modifier, ) } @@ -123,6 +138,7 @@ fun VaultAddEditCustomField( value = customField.value, onValueChanged = { onCustomFieldValueChange(customField.copy(value = it)) }, onEditValue = { shouldShowChooserDialog = true }, + cardStyle = cardStyle, modifier = modifier, ) } @@ -138,24 +154,33 @@ private fun CustomFieldBoolean( value: Boolean, onValueChanged: (Boolean) -> Unit, onEditValue: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { Row( modifier = modifier .semantics(mergeDescendants = true) {} + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(color = BitwardenTheme.colorScheme.background.pressed), + onClick = { onValueChanged(!value) }, + ) + .cardPadding(cardStyle = cardStyle, end = 4.dp, top = 6.dp, bottom = 6.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { BitwardenSwitch( label = label, isChecked = value, - onCheckedChange = onValueChanged, + onCheckedChange = null, modifier = Modifier.weight(1f), ) BitwardenRowOfActions( actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_cog, contentDescription = stringResource(id = R.string.edit), onClick = onEditValue, @@ -175,6 +200,7 @@ private fun CustomFieldHiddenField( onValueChanged: (String) -> Unit, onEditValue: () -> Unit, onVisibilityChanged: (Boolean) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { var shouldShowPassword by remember { mutableStateOf(value = false) } @@ -188,14 +214,15 @@ private fun CustomFieldHiddenField( onVisibilityChanged(shouldShowPassword) }, singleLine = true, - modifier = modifier, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_cog, contentDescription = stringResource(id = R.string.edit), onClick = onEditValue, ) }, + cardStyle = cardStyle, + modifier = modifier, ) } @@ -208,6 +235,7 @@ private fun CustomFieldTextField( value: String, onValueChanged: (String) -> Unit, onEditValue: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { BitwardenTextFieldWithActions( @@ -215,14 +243,15 @@ private fun CustomFieldTextField( value = value, onValueChange = onValueChanged, singleLine = true, - modifier = modifier, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_cog, contentDescription = stringResource(id = R.string.edit), onClick = onEditValue, ) }, + cardStyle = cardStyle, + modifier = modifier, ) } @@ -235,41 +264,33 @@ private fun CustomFieldLinkedField( selectedOption: VaultLinkedFieldType, onValueChanged: (VaultLinkedFieldType) -> Unit, onEditValue: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, supportedLinkedTypes: ImmutableList = persistentListOf(), ) { val possibleTypesWithStrings = supportedLinkedTypes.associateWith { it.label.invoke() } - - Row( - modifier = modifier - .semantics(mergeDescendants = true) {} - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - BitwardenMultiSelectButton( - label = label, - options = supportedLinkedTypes.map { it.label.invoke() }.toImmutableList(), - selectedOption = selectedOption.label.invoke(), - onOptionSelected = { selectedType -> - possibleTypesWithStrings.forEach { - if (it.value == selectedType) { - onValueChanged(it.key) - } + BitwardenMultiSelectButton( + modifier = modifier, + label = label, + options = supportedLinkedTypes.map { it.label.invoke() }.toImmutableList(), + selectedOption = selectedOption.label.invoke(), + onOptionSelected = { selectedType -> + possibleTypesWithStrings.forEach { + if (it.value == selectedType) { + onValueChanged(it.key) } - }, - modifier = Modifier.weight(1f), - ) - - BitwardenRowOfActions( - actions = { - BitwardenTonalIconButton( - vectorIconRes = R.drawable.ic_cog, - contentDescription = stringResource(id = R.string.edit), - onClick = onEditValue, - ) - }, - ) - } + } + }, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_cog, + contentDescription = stringResource(id = R.string.edit), + onClick = onEditValue, + ) + }, + actionsPadding = PaddingValues(end = 4.dp), + cardStyle = cardStyle, + ) } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt index b574cd5973..fb55a3ce41 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt @@ -13,10 +13,12 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector @@ -44,10 +46,11 @@ fun LazyListScope.vaultAddEditIdentityItems( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = commonTypeHandlers.onNameTextChange, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNameEntry", + .standardHorizontalMargin(), ) } item { @@ -58,224 +61,225 @@ fun LazyListScope.vaultAddEditIdentityItems( modifier = Modifier .testTag("IdentityTitlePicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.first_name), value = identityState.firstName, onValueChange = identityItemTypeHandlers.onFirstNameTextChange, + textFieldTestTag = "IdentityFirstNameEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityFirstNameEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.middle_name), value = identityState.middleName, onValueChange = identityItemTypeHandlers.onMiddleNameTextChange, + textFieldTestTag = "IdentityMiddleNameEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityMiddleNameEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.last_name), value = identityState.lastName, onValueChange = identityItemTypeHandlers.onLastNameTextChange, + textFieldTestTag = "IdentityLastNameEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityLastNameEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.username), value = identityState.username, onValueChange = identityItemTypeHandlers.onUsernameTextChange, + textFieldTestTag = "IdentityUsernameEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityUsernameEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.company), value = identityState.company, onValueChange = identityItemTypeHandlers.onCompanyTextChange, + textFieldTestTag = "IdentityCompanyEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityCompanyEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.ssn), value = identityState.ssn, onValueChange = identityItemTypeHandlers.onSsnTextChange, + textFieldTestTag = "IdentitySsnEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentitySsnEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.passport_number), value = identityState.passportNumber, onValueChange = identityItemTypeHandlers.onPassportNumberTextChange, + textFieldTestTag = "IdentityPassportNumberEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityPassportNumberEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.license_number), value = identityState.licenseNumber, onValueChange = identityItemTypeHandlers.onLicenseNumberTextChange, + textFieldTestTag = "IdentityLicenseNumberEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityLicenseNumberEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.email), value = identityState.email, onValueChange = identityItemTypeHandlers.onEmailTextChange, + textFieldTestTag = "IdentityEmailEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityEmailEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.phone), value = identityState.phone, onValueChange = identityItemTypeHandlers.onPhoneTextChange, + textFieldTestTag = "IdentityPhoneEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityPhoneEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.address1), value = identityState.address1, onValueChange = identityItemTypeHandlers.onAddress1TextChange, + textFieldTestTag = "IdentityAddressOneEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityAddressOneEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.address2), value = identityState.address2, onValueChange = identityItemTypeHandlers.onAddress2TextChange, + textFieldTestTag = "IdentityAddressTwoEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityAddressTwoEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.address3), value = identityState.address3, onValueChange = identityItemTypeHandlers.onAddress3TextChange, + textFieldTestTag = "IdentityAddressThreeEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityAddressThreeEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.city_town), value = identityState.city, onValueChange = identityItemTypeHandlers.onCityTextChange, + textFieldTestTag = "IdentityCityEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityCityEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.state_province), value = identityState.state, onValueChange = identityItemTypeHandlers.onStateTextChange, + textFieldTestTag = "IdentityStateEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityStateEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.zip_postal_code), value = identityState.zip, onValueChange = identityItemTypeHandlers.onZipTextChange, + textFieldTestTag = "IdentityPostalCodeEntry", + cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityPostalCodeEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.country), value = identityState.country, onValueChange = identityItemTypeHandlers.onCountryTextChange, + textFieldTestTag = "IdentityCountryEntry", + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "IdentityCountryEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.miscellaneous), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), options = commonState @@ -290,38 +294,40 @@ fun LazyListScope.vaultAddEditIdentityItems( .first { it.name == selectedFolderName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FolderPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource( id = R.string.favorite, ), isChecked = commonState.favorite, onCheckedChange = commonTypeHandlers.onToggleFavorite, + cardStyle = if (commonState.isUnlockWithPasswordEnabled) { + CardStyle.Top() + } else { + CardStyle.Full + }, modifier = Modifier .testTag("ItemFavoriteToggle") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.isUnlockWithPasswordEnabled) { item { - Spacer(modifier = Modifier.height(16.dp)) BitwardenSwitch( label = stringResource(id = R.string.password_prompt), isChecked = commonState.masterPasswordReprompt, onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt, - modifier = Modifier - .testTag("MasterPasswordRepromptToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), actions = { BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_question_circle, @@ -332,53 +338,58 @@ fun LazyListScope.vaultAddEditIdentityItems( contentColor = BitwardenTheme.colorScheme.icon.secondary, ) }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("MasterPasswordRepromptToggle") + .fillMaxWidth() + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( singleLine = false, label = stringResource(id = R.string.notes), value = commonState.notes, onValueChange = commonTypeHandlers.onNotesTextChange, + textFieldTestTag = "ItemNotesEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNotesEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.custom_fields), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) } items(commonState.customFieldData) { customItem -> - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) VaultAddEditCustomField( customField = customItem, onCustomFieldValueChange = commonTypeHandlers.onCustomFieldValueChange, onCustomFieldAction = commonTypeHandlers.onCustomFieldActionSelect, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), supportedLinkedTypes = persistentListOf( VaultLinkedFieldType.TITLE, VaultLinkedFieldType.MIDDLE_NAME, @@ -401,6 +412,10 @@ fun LazyListScope.vaultAddEditIdentityItems( VaultLinkedFieldType.FULL_NAME, ), onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -410,23 +425,24 @@ fun LazyListScope.vaultAddEditIdentityItems( onFinishNamingClick = commonTypeHandlers.onAddNewCustomFieldClick, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (isAddItemMode && commonState.hasOrganizations) { item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.ownership), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), options = commonState @@ -441,10 +457,11 @@ fun LazyListScope.vaultAddEditIdentityItems( .first { it.name == selectedOwnerName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("ItemOwnershipPicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.selectedOwnerId != null) { @@ -454,9 +471,6 @@ fun LazyListScope.vaultAddEditIdentityItems( ) } } - item { - Spacer(modifier = Modifier.height(24.dp)) - } } @Composable @@ -480,6 +494,7 @@ private fun TitleMultiSelectButton( .first { it.value.toString(resources) == selectedString }, ) }, + cardStyle = CardStyle.Top(), modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt index fcd7fc16ab..60bdd156d4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditItemContent.kt @@ -13,9 +13,11 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers @@ -57,12 +59,13 @@ fun VaultAddEditContent( ) LazyColumn(modifier = modifier) { - item { - if (state.isIndividualVaultDisabled && isAddItemMode) { + if (state.isIndividualVaultDisabled && isAddItemMode) { + item { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenInfoCalloutCard( text = stringResource(R.string.personal_ownership_policy_in_effect), modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("PersonalOwnershipPolicyLabel") .fillMaxWidth(), ) @@ -70,23 +73,25 @@ fun VaultAddEditContent( } 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)) } if (isAddItemMode) { item { - Spacer(modifier = Modifier.height(8.dp)) TypeOptionsItem( entries = typeOptions, itemType = state.type, onTypeOptionClicked = onTypeOptionClicked, modifier = Modifier .testTag("ItemTypePicker") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } @@ -148,6 +153,7 @@ fun VaultAddEditContent( } item { + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -173,6 +179,7 @@ private fun TypeOptionsItem( .key onTypeOptionClicked(selectedOptionId) }, + cardStyle = CardStyle.Full, modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index 6700b13aaf..422c0ed7b7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,9 +19,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenHiddenPasswordField @@ -28,6 +30,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWi import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -51,15 +54,15 @@ fun LazyListScope.vaultAddEditLoginItems( onTotpSetupClick: () -> Unit, ) { item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = commonActionHandler.onNameTextChange, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNameEntry", + .standardHorizontalMargin(), ) } @@ -68,15 +71,20 @@ fun LazyListScope.vaultAddEditLoginItems( UsernameRow( username = loginState.username, loginItemTypeHandlers = loginItemTypeHandlers, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) PasswordRow( password = loginState.password, canViewPassword = loginState.canViewPassword, loginItemTypeHandlers = loginItemTypeHandlers, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -85,23 +93,25 @@ fun LazyListScope.vaultAddEditLoginItems( Spacer(modifier = Modifier.height(8.dp)) PasskeyField( creationDateTime = creationDateTime, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), canRemovePasskey = loginState.canRemovePasskey, loginItemTypeHandlers = loginItemTypeHandlers, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.authenticator_key), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } if (loginState.totp != null) { @@ -109,7 +119,7 @@ fun LazyListScope.vaultAddEditLoginItems( BitwardenTextFieldWithActions( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), label = stringResource(id = R.string.totp), value = loginState.totp, trailingIconContent = { @@ -123,20 +133,21 @@ fun LazyListScope.vaultAddEditLoginItems( readOnly = true, singleLine = true, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_totp), onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(loginState.totp) }, ) - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_camera, contentDescription = stringResource(id = R.string.camera), onClick = onTotpSetupClick, ) }, textFieldTestTag = "LoginTotpEntry", + cardStyle = CardStyle.Full, ) } } else { @@ -149,27 +160,32 @@ fun LazyListScope.vaultAddEditLoginItems( modifier = Modifier .testTag("SetupTotpButton") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.ur_is), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(loginState.uriList) { uriItem -> - Spacer(modifier = Modifier.height(8.dp)) + itemsIndexed(loginState.uriList) { index, uriItem -> VaultAddEditUriItem( uriItem = uriItem, onUriValueChange = loginItemTypeHandlers.onUriValueChange, onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick, + cardStyle = loginState.uriList.toListItemCardStyle(index = index), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -181,22 +197,23 @@ fun LazyListScope.vaultAddEditLoginItems( modifier = Modifier .testTag("LoginAddNewUriButton") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.miscellaneous), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), options = commonState @@ -211,37 +228,39 @@ fun LazyListScope.vaultAddEditLoginItems( .first { it.name == selectedFolderName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FolderPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource( id = R.string.favorite, ), isChecked = commonState.favorite, onCheckedChange = commonActionHandler.onToggleFavorite, + cardStyle = if (commonState.isUnlockWithPasswordEnabled) { + CardStyle.Top() + } else { + CardStyle.Full + }, modifier = Modifier .testTag("ItemFavoriteToggle") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.isUnlockWithPasswordEnabled) { item { - Spacer(modifier = Modifier.height(16.dp)) BitwardenSwitch( label = stringResource(id = R.string.password_prompt), isChecked = commonState.masterPasswordReprompt, onCheckedChange = commonActionHandler.onToggleMasterPasswordReprompt, - modifier = Modifier - .testTag("MasterPasswordRepromptToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), actions = { BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_question_circle_small, @@ -252,58 +271,67 @@ fun LazyListScope.vaultAddEditLoginItems( contentColor = BitwardenTheme.colorScheme.icon.secondary, ) }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("MasterPasswordRepromptToggle") + .fillMaxWidth() + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( singleLine = false, label = stringResource(id = R.string.notes), value = commonState.notes, onValueChange = commonActionHandler.onNotesTextChange, + textFieldTestTag = "ItemNotesEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNotesEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.custom_fields), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) } items(commonState.customFieldData) { customItem -> - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) VaultAddEditCustomField( customField = customItem, onCustomFieldValueChange = commonActionHandler.onCustomFieldValueChange, onCustomFieldAction = commonActionHandler.onCustomFieldActionSelect, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), supportedLinkedTypes = persistentListOf( VaultLinkedFieldType.PASSWORD, VaultLinkedFieldType.USERNAME, ), onHiddenVisibilityChanged = commonActionHandler.onHiddenFieldVisibilityChange, + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -313,23 +341,24 @@ fun LazyListScope.vaultAddEditLoginItems( onFinishNamingClick = commonActionHandler.onAddNewCustomFieldClick, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (isAddItemMode && commonState.hasOrganizations) { item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.ownership), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), options = commonState @@ -344,10 +373,11 @@ fun LazyListScope.vaultAddEditLoginItems( .first { it.name == selectedOwnerName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("ItemOwnershipPicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -358,16 +388,13 @@ fun LazyListScope.vaultAddEditLoginItems( ) } } - - item { - Spacer(modifier = Modifier.height(24.dp)) - } } @Composable private fun UsernameRow( username: String, loginItemTypeHandlers: VaultAddEditLoginTypeHandlers, + modifier: Modifier = Modifier, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(false) } @@ -376,7 +403,7 @@ private fun UsernameRow( value = username, onValueChange = loginItemTypeHandlers.onUsernameTextChange, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_generate, contentDescription = stringResource(id = R.string.generate_username), onClick = { @@ -389,9 +416,9 @@ private fun UsernameRow( modifier = Modifier.testTag(tag = "GenerateUsernameButton"), ) }, - modifier = Modifier - .padding(horizontal = 16.dp), textFieldTestTag = "LoginUsernameEntry", + cardStyle = CardStyle.Top(dividerPadding = 0.dp), + modifier = modifier, ) if (shouldShowDialog) { @@ -423,6 +450,7 @@ private fun PasswordRow( password: String, canViewPassword: Boolean, loginItemTypeHandlers: VaultAddEditLoginTypeHandlers, + modifier: Modifier = Modifier, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(false) } @@ -439,17 +467,16 @@ private fun PasswordRow( }, showPasswordTestTag = "ViewPasswordButton", passwordFieldTestTag = "LoginPasswordEntry", - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + cardStyle = CardStyle.Bottom, + modifier = modifier, ) { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_check_mark, contentDescription = stringResource(id = R.string.check_password), onClick = loginItemTypeHandlers.onPasswordCheckerClick, modifier = Modifier.testTag(tag = "CheckPasswordButton"), ) - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_generate, contentDescription = stringResource(id = R.string.generate_password), onClick = { @@ -488,10 +515,8 @@ private fun PasswordRow( BitwardenHiddenPasswordField( label = stringResource(id = R.string.password), value = password, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .testTag("LoginPasswordEntry"), + cardStyle = CardStyle.Bottom, + modifier = modifier.testTag("LoginPasswordEntry"), ) } } @@ -509,10 +534,9 @@ private fun PasskeyField( onValueChange = { }, readOnly = true, singleLine = true, - modifier = modifier, actions = { if (canRemovePasskey) { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_minus, contentDescription = stringResource(id = R.string.remove_passkey), onClick = loginItemTypeHandlers.onClearFido2CredentialClick, @@ -520,5 +544,7 @@ private fun PasskeyField( ) } }, + cardStyle = CardStyle.Full, + modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt index 392fde2583..a056065bdf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt @@ -11,10 +11,12 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector @@ -38,25 +40,27 @@ fun LazyListScope.vaultAddEditSecureNotesItems( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = commonTypeHandlers.onNameTextChange, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNameEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.miscellaneous), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), options = commonState @@ -71,36 +75,38 @@ fun LazyListScope.vaultAddEditSecureNotesItems( .first { it.name == selectedFolderName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FolderPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource(id = R.string.favorite), isChecked = commonState.favorite, onCheckedChange = commonTypeHandlers.onToggleFavorite, + cardStyle = if (commonState.isUnlockWithPasswordEnabled) { + CardStyle.Top() + } else { + CardStyle.Full + }, modifier = Modifier .testTag("ItemFavoriteToggle") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.isUnlockWithPasswordEnabled) { item { - Spacer(modifier = Modifier.height(16.dp)) BitwardenSwitch( label = stringResource(id = R.string.password_prompt), isChecked = commonState.masterPasswordReprompt, onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt, - modifier = Modifier - .testTag("MasterPasswordRepromptToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), actions = { BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_question_circle, @@ -111,53 +117,61 @@ fun LazyListScope.vaultAddEditSecureNotesItems( contentColor = BitwardenTheme.colorScheme.icon.secondary, ) }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("MasterPasswordRepromptToggle") + .fillMaxWidth() + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( singleLine = false, label = stringResource(id = R.string.notes), value = commonState.notes, onValueChange = commonTypeHandlers.onNotesTextChange, + textFieldTestTag = "ItemNotesEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNotesEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.custom_fields), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } items(commonState.customFieldData) { customItem -> - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) VaultAddEditCustomField( customField = customItem, onCustomFieldValueChange = commonTypeHandlers.onCustomFieldValueChange, onCustomFieldAction = commonTypeHandlers.onCustomFieldActionSelect, + onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange, + .standardHorizontalMargin(), ) } @@ -172,23 +186,24 @@ fun LazyListScope.vaultAddEditSecureNotesItems( ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (isAddItemMode && commonState.hasOrganizations) { item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.ownership), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), options = commonState @@ -203,10 +218,11 @@ fun LazyListScope.vaultAddEditSecureNotesItems( .first { it.name == selectedOwnerName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("ItemOwnershipPicker") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -217,8 +233,4 @@ fun LazyListScope.vaultAddEditSecureNotesItems( ) } } - - item { - Spacer(modifier = Modifier.height(24.dp)) - } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt index 052d95b696..06e41a04bc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSshKeyItems.kt @@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectB import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers @@ -44,10 +45,11 @@ fun LazyListScope.vaultAddEditSshKeyItems( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = commonTypeHandlers.onNameTextChange, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), - textFieldTestTag = "ItemNameEntry", ) } @@ -58,15 +60,15 @@ fun LazyListScope.vaultAddEditSshKeyItems( value = sshKeyState.publicKey, readOnly = true, onValueChange = { }, + textFieldTestTag = "PublicKeyEntry", + cardStyle = CardStyle.Top(), modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), - textFieldTestTag = "PublicKeyEntry", ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordField( label = stringResource(id = R.string.private_key), value = sshKeyState.privateKey, @@ -75,6 +77,7 @@ fun LazyListScope.vaultAddEditSshKeyItems( showPassword = sshKeyState.showPrivateKey, showPasswordChange = { sshKeyTypeHandlers.onPrivateKeyVisibilityChange(it) }, showPasswordTestTag = "ViewPrivateKeyButton", + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("PrivateKeyEntry") .fillMaxWidth() @@ -83,31 +86,32 @@ fun LazyListScope.vaultAddEditSshKeyItems( } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.fingerprint), value = sshKeyState.fingerprint, readOnly = true, onValueChange = { /* no-op */ }, + textFieldTestTag = "FingerprintEntry", + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), - textFieldTestTag = "FingerprintEntry", ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.miscellaneous), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), options = commonState @@ -122,37 +126,39 @@ fun LazyListScope.vaultAddEditSshKeyItems( .first { it.name == selectedFolderName }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("FolderPicker") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) BitwardenSwitch( label = stringResource( id = R.string.favorite, ), isChecked = commonState.favorite, onCheckedChange = commonTypeHandlers.onToggleFavorite, + cardStyle = if (commonState.isUnlockWithPasswordEnabled) { + CardStyle.Top() + } else { + CardStyle.Full + }, modifier = Modifier .testTag("ItemFavoriteToggle") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } if (commonState.isUnlockWithPasswordEnabled) { item { - Spacer(modifier = Modifier.height(16.dp)) BitwardenSwitch( label = stringResource(id = R.string.password_prompt), isChecked = commonState.masterPasswordReprompt, onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt, - modifier = Modifier - .testTag("MasterPasswordRepromptToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), actions = { BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_question_circle_small, @@ -163,54 +169,63 @@ fun LazyListScope.vaultAddEditSshKeyItems( contentColor = BitwardenTheme.colorScheme.icon.secondary, ) }, + cardStyle = CardStyle.Bottom, + modifier = Modifier + .testTag("MasterPasswordRepromptToggle") + .fillMaxWidth() + .standardHorizontalMargin(), ) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( singleLine = false, label = stringResource(id = R.string.notes), value = commonState.notes, onValueChange = commonTypeHandlers.onNotesTextChange, + textFieldTestTag = "ItemNotesEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNotesEntry", + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.custom_fields), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) } items(commonState.customFieldData) { customItem -> - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) VaultAddEditCustomField( customField = customItem, onCustomFieldValueChange = commonTypeHandlers.onCustomFieldValueChange, onCustomFieldAction = commonTypeHandlers.onCustomFieldActionSelect, + onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange, + .standardHorizontalMargin(), ) } @@ -225,7 +240,7 @@ fun LazyListScope.vaultAddEditSshKeyItems( ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt index e4d8a24e5a..cbfc669e01 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -9,13 +8,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType @@ -30,6 +29,8 @@ fun VaultAddEditUriItem( uriItem: UriItem, onUriItemRemoved: (UriItem) -> Unit, onUriValueChange: (UriItem) -> Unit, + cardStyle: CardStyle, + modifier: Modifier = Modifier, ) { var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) } var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) } @@ -39,16 +40,16 @@ fun VaultAddEditUriItem( value = uriItem.uri.orEmpty(), onValueChange = { onUriValueChange(uriItem.copy(uri = it)) }, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_cog, contentDescription = stringResource(id = R.string.options), onClick = { shouldShowOptionsDialog = true }, modifier = Modifier.testTag(tag = "LoginUriOptionsButton"), ) }, - modifier = Modifier - .padding(horizontal = 16.dp), textFieldTestTag = "LoginUriEntry", + cardStyle = cardStyle, + modifier = modifier, ) if (shouldShowOptionsDialog) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt index 422cef1406..91d281bfa5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -24,11 +24,15 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers @@ -54,35 +58,40 @@ fun AttachmentsContent( modifier = Modifier .testTag("NoAttachmentsLabel") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(8.dp)) } } else { - items(viewState.attachments) { + item { + Spacer(modifier = Modifier.height(height = 12.dp)) + } + itemsIndexed(viewState.attachments) { index, it -> AttachmentListEntry( attachmentItem = it, onDeleteClick = attachmentsHandlers.onDeleteClick, + cardStyle = viewState.attachments.toListItemCardStyle(index = index), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("AttachmentList"), ) } } item { - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.add_new_attachment), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { - Spacer(modifier = Modifier.height(16.dp)) Text( text = viewState .newAttachment @@ -93,7 +102,7 @@ fun AttachmentsContent( textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("SelectedFileNameLabel"), ) } @@ -105,7 +114,7 @@ fun AttachmentsContent( onClick = attachmentsHandlers.onChooseFileClick, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .testTag("AttachmentSelectFileButton"), ) Spacer(modifier = Modifier.height(4.dp)) @@ -115,11 +124,13 @@ fun AttachmentsContent( style = BitwardenTheme.typography.bodySmall, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 32.dp), + .standardHorizontalMargin() + .padding(horizontal = 12.dp), ) } item { + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -129,6 +140,7 @@ fun AttachmentsContent( private fun AttachmentListEntry( attachmentItem: AttachmentsState.AttachmentItem, onDeleteClick: (attachmentId: String) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { var shouldShowDeleteDialog by rememberSaveable { mutableStateOf(false) } @@ -148,12 +160,11 @@ private fun AttachmentListEntry( } Row( - modifier = Modifier - .bottomDivider(paddingStart = 16.dp) - .defaultMinSize(minHeight = 56.dp) - .testTag("AttachmentRow") - .padding(vertical = 8.dp) - .then(modifier), + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, start = 16.dp) + .testTag("AttachmentRow"), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt index 2c24c73270..9c5a70ca76 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemAttachmentContent.kt @@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text @@ -19,10 +18,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -33,15 +34,16 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme fun AttachmentItemContent( attachmentItem: VaultItemState.ViewState.Content.Common.AttachmentItem, onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { var shouldShowPremiumWarningDialog by rememberSaveable { mutableStateOf(false) } var shouldShowSizeWarningDialog by rememberSaveable { mutableStateOf(false) } Row( modifier = modifier - .bottomDivider() - .defaultMinSize(minHeight = 56.dp) - .padding(vertical = 8.dp) + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) + .cardPadding(cardStyle = cardStyle, start = 16.dp) .testTag("CipherAttachment"), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index 6737e55b5b..1215b01e06 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -7,17 +7,21 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand @@ -37,46 +41,53 @@ fun VaultItemCardContent( ) { 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 { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = { }, readOnly = true, singleLine = false, + textFieldTestTag = "CardItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "CardItemNameEntry", + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } cardState.cardholderName?.let { cardholderName -> item { - Spacer(modifier = Modifier.height(8.dp)) 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), + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "CardholderNameEntry", + .standardHorizontalMargin(), ) } } cardState.number?.let { numberData -> item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordFieldWithActions( label = stringResource(id = R.string.number), value = numberData.number, @@ -86,7 +97,7 @@ fun VaultItemCardContent( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_number), onClick = vaultCardItemTypeHandlers.onCopyNumberClick, @@ -95,50 +106,62 @@ fun VaultItemCardContent( }, passwordFieldTestTag = "CardNumberEntry", showPasswordTestTag = "CardViewNumberButton", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = numberData), + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) { item { - Spacer(modifier = Modifier.height(8.dp)) 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), + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "CardBrandEntry", + .standardHorizontalMargin(), ) } } cardState.expiration?.let { expiration -> item { - Spacer(modifier = Modifier.height(8.dp)) 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), + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "CardExpirationEntry", + .standardHorizontalMargin(), ) } } cardState.securityCode?.let { securityCodeData -> item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordFieldWithActions( label = stringResource(id = R.string.security_code), value = securityCodeData.code, @@ -148,29 +171,35 @@ fun VaultItemCardContent( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_security_code), onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick, modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"), ) }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), showPasswordTestTag = "CardViewSecurityCodeButton", passwordFieldTestTag = "CardSecurityCodeEntry", + cardStyle = cardState + .propertyList + .toListItemCardStyle( + index = cardState.propertyList.indexOf(element = securityCodeData), + ), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) } } commonState.notes?.let { notes -> item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) @@ -181,7 +210,7 @@ fun VaultItemCardContent( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_notes), onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, @@ -189,69 +218,75 @@ fun VaultItemCardContent( ) }, textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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(8.dp)) + 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() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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)) } - items(attachments) { attachmentItem -> + itemsIndexed(attachments) { index, attachmentItem -> AttachmentItemContent( modifier = Modifier .testTag("CipherAttachment") .fillMaxWidth() - .padding(start = 16.dp), + .standardHorizontalMargin(), attachmentItem = attachmentItem, - onAttachmentDownloadClick = - vaultCommonItemTypeHandlers.onAttachmentDownloadClick, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), ) } - item { Spacer(modifier = Modifier.height(8.dp)) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin() + .padding(horizontal = 12.dp), ) } item { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCustomField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCustomField.kt index 424760ef6a..97549083af 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCustomField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCustomField.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton +import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter @@ -15,13 +17,17 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter /** * Custom Field UI common for all item types. */ -@Suppress("LongMethod", "MaxLineLength") +@Suppress("LongMethod") @Composable fun CustomField( customField: VaultItemState.ViewState.Content.Common.Custom, onCopyCustomHiddenField: (String) -> Unit, onCopyCustomTextField: (String) -> Unit, - onShowHiddenFieldClick: (VaultItemState.ViewState.Content.Common.Custom.HiddenField, Boolean) -> Unit, + onShowHiddenFieldClick: ( + VaultItemState.ViewState.Content.Common.Custom.HiddenField, + Boolean, + ) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { when (customField) { @@ -31,30 +37,44 @@ fun CustomField( isChecked = customField.value, readOnly = true, onCheckedChange = { }, + cardStyle = cardStyle, modifier = modifier, ) } is VaultItemState.ViewState.Content.Common.Custom.HiddenField -> { - BitwardenPasswordFieldWithActions( - label = customField.name, - value = customField.value, - showPasswordChange = { onShowHiddenFieldClick(customField, it) }, - showPassword = customField.isVisible, - onValueChange = { }, - readOnly = true, - singleLine = false, - modifier = modifier, - actions = { - if (customField.isCopyable) { - BitwardenTonalIconButton( + if (customField.isCopyable) { + BitwardenPasswordFieldWithActions( + label = customField.name, + value = customField.value, + showPasswordChange = { onShowHiddenFieldClick(customField, it) }, + showPassword = customField.isVisible, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy), onClick = { onCopyCustomHiddenField(customField.value) }, ) - } - }, - ) + }, + cardStyle = cardStyle, + modifier = modifier, + ) + } else { + BitwardenPasswordField( + label = customField.name, + value = customField.value, + showPasswordChange = { onShowHiddenFieldClick(customField, it) }, + showPassword = customField.isVisible, + onValueChange = { }, + readOnly = true, + singleLine = false, + cardStyle = cardStyle, + modifier = modifier, + ) + } } is VaultItemState.ViewState.Content.Common.Custom.LinkedField -> { @@ -68,6 +88,7 @@ fun CustomField( onValueChange = { }, readOnly = true, singleLine = false, + cardStyle = cardStyle, modifier = modifier, ) } @@ -79,16 +100,17 @@ fun CustomField( onValueChange = { }, readOnly = true, singleLine = false, - modifier = modifier, actions = { if (customField.isCopyable) { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy), onClick = { onCopyCustomTextField(customField.value) }, ) } }, + cardStyle = cardStyle, + modifier = modifier, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index d165ace283..4a48883d06 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -7,16 +7,20 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers @@ -34,30 +38,33 @@ fun VaultItemIdentityContent( ) { 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 { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = { }, readOnly = true, singleLine = false, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNameEntry", + .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } identityState.identityName?.let { identityName -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.identity_name), value = identityName, @@ -65,15 +72,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityNameEntry", copyActionTestTag = "IdentityCopyNameButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = identityName), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.username?.let { username -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.username), value = username, @@ -81,15 +93,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityUsernameEntry", copyActionTestTag = "IdentityCopyUsernameButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = username), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.company?.let { company -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.company), value = company, @@ -97,15 +114,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityCompanyEntry", copyActionTestTag = "IdentityCopyCompanyButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = company), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.ssn?.let { ssn -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.ssn), value = ssn, @@ -113,15 +135,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentitySsnEntry", copyActionTestTag = "IdentityCopySsnButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = ssn), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.passportNumber?.let { passportNumber -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.passport_number), value = passportNumber, @@ -129,15 +156,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityPassportNumberEntry", copyActionTestTag = "IdentityCopyPassportNumberButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = passportNumber), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.licenseNumber?.let { licenseNumber -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.license_number), value = licenseNumber, @@ -145,15 +177,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityLicenseNumberEntry", copyActionTestTag = "IdentityCopyLicenseNumberButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = licenseNumber), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.email?.let { email -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.email), value = email, @@ -161,15 +198,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityEmailEntry", copyActionTestTag = "IdentityCopyEmailButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = email), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.phone?.let { phone -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.phone), value = phone, @@ -177,15 +219,20 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityPhoneEntry", copyActionTestTag = "IdentityCopyPhoneButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = phone), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } identityState.address?.let { address -> item { - Spacer(modifier = Modifier.height(8.dp)) IdentityCopyField( label = stringResource(id = R.string.address), value = address, @@ -193,19 +240,26 @@ fun VaultItemIdentityContent( textFieldTestTag = "IdentityAddressEntry", copyActionTestTag = "IdentityCopyAddressButton", onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick, + cardStyle = identityState + .propertyList + .toListItemCardStyle( + index = identityState.propertyList.indexOf(element = address), + dividerPadding = 0.dp, + ), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.notes?.let { notes -> item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) @@ -216,69 +270,75 @@ fun VaultItemIdentityContent( textFieldTestTag = "CipherNotesLabel", copyActionTestTag = "CipherNotesCopyButton", onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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(8.dp)) + 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() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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)) } - items(attachments) { attachmentItem -> + itemsIndexed(attachments) { index, attachmentItem -> AttachmentItemContent( modifier = Modifier .testTag("CipherAttachment") .fillMaxWidth() - .padding(start = 16.dp), + .standardHorizontalMargin(), attachmentItem = attachmentItem, - onAttachmentDownloadClick = - vaultCommonItemTypeHandlers.onAttachmentDownloadClick, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), ) } - item { Spacer(modifier = Modifier.height(8.dp)) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin() + .padding(horizontal = 12.dp), ) } item { @@ -296,6 +356,7 @@ private fun IdentityCopyField( textFieldTestTag: String, copyActionTestTag: String, onCopyClick: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { BitwardenTextFieldWithActions( @@ -305,14 +366,15 @@ private fun IdentityCopyField( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = copyContentDescription, onClick = onCopyClick, modifier = Modifier.testTag(tag = copyActionTestTag), ) }, - modifier = modifier, textFieldTestTag = textFieldTestTag, + cardStyle = cardStyle, + modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 03163fe6ec..5c9105e702 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -17,13 +18,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenHiddenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers @@ -34,7 +38,7 @@ private const val AUTH_CODE_SPACING_INTERVAL = 3 /** * The top level content UI state for the [VaultItemScreen] when viewing a Login cipher. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun VaultItemLoginContent( commonState: VaultItemState.ViewState.Content.Common, @@ -47,52 +51,73 @@ fun VaultItemLoginContent( 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 { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = { }, readOnly = true, singleLine = false, + textFieldTestTag = "LoginItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "LoginItemNameEntry", + .standardHorizontalMargin(), ) } + 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 { - Spacer(modifier = Modifier.height(8.dp)) UsernameField( username = username, onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick, + cardStyle = loginItemState + .passwordData?.let { CardStyle.Top(dividerPadding = 0.dp) } + ?: CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } loginItemState.passwordData?.let { passwordData -> item { - Spacer(modifier = Modifier.height(8.dp)) PasswordField( passwordData = passwordData, onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick, onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick, onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick, + cardStyle = loginItemState + .username?.let { CardStyle.Bottom } + ?: CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } @@ -104,7 +129,7 @@ fun VaultItemLoginContent( creationDate = creationDate.invoke(), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } @@ -118,41 +143,45 @@ fun VaultItemLoginContent( onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris -> item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( - label = stringResource(id = R.string.ur_is), + label = stringResource(id = R.string.autofill_options), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(uris) { uriData -> - Spacer(modifier = Modifier.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() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.notes?.let { notes -> item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) @@ -168,59 +197,64 @@ fun VaultItemLoginContent( commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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(8.dp)) + 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() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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)) } - items(attachments) { attachmentItem -> + itemsIndexed(attachments) { index, attachmentItem -> AttachmentItemContent( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp), + .standardHorizontalMargin(), attachmentItem = attachmentItem, - onAttachmentDownloadClick = - vaultCommonItemTypeHandlers.onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, ) } - item { Spacer(modifier = Modifier.height(8.dp)) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin() + .padding(horizontal = 12.dp), ) } @@ -231,7 +265,8 @@ fun VaultItemLoginContent( text = revisionDate, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin() + .padding(horizontal = 12.dp), ) } } @@ -243,7 +278,8 @@ fun VaultItemLoginContent( onPasswordHistoryClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin() + .padding(horizontal = 12.dp), ) } } @@ -266,6 +302,7 @@ private fun Fido2CredentialField( onValueChange = { }, readOnly = true, singleLine = true, + cardStyle = CardStyle.Full, modifier = modifier, ) } @@ -283,7 +320,7 @@ private fun NotesField( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_notes), onClick = onCopyAction, @@ -291,6 +328,7 @@ private fun NotesField( ) }, textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, modifier = modifier, ) } @@ -301,6 +339,7 @@ private fun PasswordField( onShowPasswordClick: (Boolean) -> Unit, onCheckForBreachClick: () -> Unit, onCopyPasswordClick: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { if (passwordData.canViewPassword) { @@ -313,7 +352,7 @@ private fun PasswordField( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_check_mark, contentDescription = stringResource( id = R.string.check_known_data_breaches_for_this_password, @@ -321,21 +360,23 @@ private fun PasswordField( onClick = onCheckForBreachClick, modifier = Modifier.testTag(tag = "LoginCheckPasswordButton"), ) - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_password), onClick = onCopyPasswordClick, modifier = Modifier.testTag(tag = "LoginCopyPasswordButton"), ) }, - modifier = modifier, showPasswordTestTag = "LoginViewPasswordButton", passwordFieldTestTag = "LoginPasswordEntry", + cardStyle = cardStyle, + modifier = modifier, ) } else { BitwardenHiddenPasswordField( label = stringResource(id = R.string.password), value = passwordData.password, + cardStyle = cardStyle, modifier = modifier .testTag("LoginPasswordEntry"), ) @@ -373,39 +414,41 @@ private fun TotpField( modifier: Modifier = Modifier, ) { if (enabled) { - Row { - BitwardenTextFieldWithActions( - label = stringResource(id = R.string.verification_code_totp), - value = totpCodeItemData.verificationCode - .chunked(AUTH_CODE_SPACING_INTERVAL) - .joinToString(" "), - onValueChange = { }, - readOnly = true, - singleLine = true, - actions = { - BitwardenCircularCountdownIndicator( - timeLeftSeconds = totpCodeItemData.timeLeftSeconds, - periodSeconds = totpCodeItemData.periodSeconds, - ) - BitwardenTonalIconButton( - vectorIconRes = R.drawable.ic_copy, - contentDescription = stringResource(id = R.string.copy_totp), - onClick = onCopyTotpClick, - modifier = Modifier.testTag(tag = "LoginCopyTotpButton"), - ) - }, - modifier = modifier, - textFieldTestTag = "LoginTotpEntry", - ) - } + BitwardenTextFieldWithActions( + label = stringResource(id = R.string.verification_code_totp), + value = totpCodeItemData.verificationCode + .chunked(AUTH_CODE_SPACING_INTERVAL) + .joinToString(" "), + onValueChange = { }, + textStyle = BitwardenTheme.typography.sensitiveInfoSmall, + readOnly = true, + singleLine = true, + actions = { + BitwardenCircularCountdownIndicator( + timeLeftSeconds = totpCodeItemData.timeLeftSeconds, + periodSeconds = totpCodeItemData.periodSeconds, + ) + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_totp), + onClick = onCopyTotpClick, + modifier = Modifier.testTag(tag = "LoginCopyTotpButton"), + ) + }, + textFieldTestTag = "LoginTotpEntry", + cardStyle = CardStyle.Full, + modifier = modifier, + ) } else { BitwardenTextField( label = stringResource(id = R.string.verification_code_totp), - value = stringResource(id = R.string.premium_subscription_required), + value = "", + supportingText = stringResource(id = R.string.premium_subscription_required), enabled = false, singleLine = false, onValueChange = { }, readOnly = true, + cardStyle = CardStyle.Full, modifier = modifier, ) } @@ -416,6 +459,7 @@ private fun UriField( uriData: VaultItemState.ViewState.Content.ItemType.Login.UriData, onCopyUriClick: (String) -> Unit, onLaunchUriClick: (String) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { BitwardenTextFieldWithActions( @@ -426,7 +470,7 @@ private fun UriField( singleLine = false, actions = { if (uriData.isLaunchable) { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_external_link, contentDescription = stringResource(id = R.string.launch), onClick = { onLaunchUriClick(uriData.uri) }, @@ -434,7 +478,7 @@ private fun UriField( ) } if (uriData.isCopyable) { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy), onClick = { onCopyUriClick(uriData.uri) }, @@ -442,8 +486,9 @@ private fun UriField( ) } }, - modifier = modifier, textFieldTestTag = "LoginUriEntry", + cardStyle = cardStyle, + modifier = modifier, ) } @@ -451,6 +496,7 @@ private fun UriField( private fun UsernameField( username: String, onCopyUsernameClick: () -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, ) { BitwardenTextFieldWithActions( @@ -460,14 +506,15 @@ private fun UsernameField( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_username), onClick = onCopyUsernameClick, modifier = Modifier.testTag(tag = "LoginCopyUsernameButton"), ) }, - modifier = modifier, textFieldTestTag = "LoginUsernameEntry", + cardStyle = cardStyle, + modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index e3ffc46803..9ec330e68b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -16,10 +17,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers @@ -35,36 +39,40 @@ fun VaultItemSecureNoteContent( ) { LazyColumn(modifier = modifier) { item { + Spacer(modifier = Modifier.height(height = 16.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 { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = { }, readOnly = true, singleLine = false, + textFieldTestTag = "ItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "ItemNameEntry", + .standardHorizontalMargin(), ) } commonState.notes?.let { notes -> item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) @@ -75,7 +83,7 @@ fun VaultItemSecureNoteContent( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_notes), onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, @@ -83,68 +91,74 @@ fun VaultItemSecureNoteContent( ) }, textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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(8.dp)) + 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() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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)) } - items(attachments) { attachmentItem -> + itemsIndexed(attachments) { index, attachmentItem -> AttachmentItemContent( modifier = Modifier .testTag("CipherAttachment") .fillMaxWidth() - .padding(start = 16.dp), + .standardHorizontalMargin(), attachmentItem = attachmentItem, - onAttachmentDownloadClick = - vaultCommonItemTypeHandlers.onAttachmentDownloadClick, + onAttachmentDownloadClick = vaultCommonItemTypeHandlers + .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), ) } - item { Spacer(modifier = Modifier.height(8.dp)) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .standardHorizontalMargin() + .padding(horizontal = 12.dp) .semantics(mergeDescendants = true) { }, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt index 6c61e4b1be..49acc612c0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSshKeyContent.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -14,11 +15,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers @@ -36,26 +39,29 @@ fun VaultItemSshKeyContent( ) { 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 { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextField( label = stringResource(id = R.string.name), value = commonState.name, onValueChange = { }, readOnly = true, singleLine = false, + textFieldTestTag = "SshKeyItemNameEntry", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - textFieldTestTag = "SshKeyItemNameEntry", + .standardHorizontalMargin(), ) } @@ -68,13 +74,14 @@ fun VaultItemSshKeyContent( singleLine = false, readOnly = true, actions = { - BitwardenTonalIconButton( + 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() @@ -83,7 +90,6 @@ fun VaultItemSshKeyContent( } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenPasswordFieldWithActions( label = stringResource(id = R.string.private_key), value = sshKeyItemState.privateKey, @@ -91,7 +97,7 @@ fun VaultItemSshKeyContent( singleLine = false, readOnly = true, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_private_key), onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick, @@ -101,15 +107,15 @@ fun VaultItemSshKeyContent( showPassword = sshKeyItemState.showPrivateKey, showPasswordTestTag = "ViewPrivateKeyButton", showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick, + cardStyle = CardStyle.Middle(), modifier = Modifier .testTag("SshKeyItemPrivateKeyEntry") .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } item { - Spacer(modifier = Modifier.height(8.dp)) BitwardenTextFieldWithActions( label = stringResource(id = R.string.fingerprint), value = sshKeyItemState.fingerprint, @@ -117,13 +123,14 @@ fun VaultItemSshKeyContent( singleLine = false, readOnly = true, actions = { - BitwardenTonalIconButton( + 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() @@ -133,12 +140,13 @@ fun VaultItemSshKeyContent( commonState.notes?.let { notes -> item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.notes), modifier = Modifier .fillMaxWidth() - .standardHorizontalMargin(), + .standardHorizontalMargin() + .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) BitwardenTextFieldWithActions( @@ -148,7 +156,7 @@ fun VaultItemSshKeyContent( readOnly = true, singleLine = false, actions = { - BitwardenTonalIconButton( + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy_notes), onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, @@ -156,68 +164,74 @@ fun VaultItemSshKeyContent( ) }, textFieldTestTag = "CipherNotesLabel", + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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(8.dp)) + 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() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments -> item { - Spacer(modifier = Modifier.height(4.dp)) + 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)) } - items(attachments) { attachmentItem -> + itemsIndexed(attachments) { index, attachmentItem -> AttachmentItemContent( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp), + .standardHorizontalMargin(), attachmentItem = attachmentItem, onAttachmentDownloadClick = vaultCommonItemTypeHandlers .onAttachmentDownloadClick, + cardStyle = attachments.toListItemCardStyle(index = index), ) } - item { Spacer(modifier = Modifier.height(8.dp)) } } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(height = 16.dp)) VaultItemUpdateText( header = "${stringResource(id = R.string.date_updated)}: ", text = commonState.lastUpdated, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .standardHorizontalMargin() + .padding(horizontal = 12.dp) .testTag("SshKeyItemLastUpdated"), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 8f65248184..afadc4616f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -26,6 +26,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.concat +import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState @@ -1516,6 +1517,15 @@ data class VaultItemState( val fido2CredentialCreationDateText: Text?, ) : ItemType() { + /** + * Indicates that at least one of the login credentials are present. + */ + val hasLoginCredentials: Boolean + get() = username != null || + passwordData != null || + fido2CredentialCreationDateText != null || + totpCodeItemData != null + /** * A wrapper for the password data. * @@ -1571,7 +1581,24 @@ data class VaultItemState( val email: String?, val phone: String?, val address: String?, - ) : ItemType() + ) : ItemType() { + + /** + * An ordered list of Card specific elements. + */ + val propertyList: List + get() = persistentListOfNotNull( + identityName, + username, + company, + ssn, + passportNumber, + licenseNumber, + email, + phone, + address, + ) + } /** * Represents the `Card` item type. @@ -1590,6 +1617,18 @@ data class VaultItemState( val securityCode: CodeData?, ) : ItemType() { + /** + * An ordered list of Card specific elements. + */ + val propertyList: List + get() = persistentListOfNotNull( + cardholderName, + number, + brand.takeIf { brand != VaultCardBrand.SELECT }, + expiration, + securityCode, + ) + /** * A wrapper for the number data. * diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index 86c0a684d1..5b82f2394b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -17,10 +17,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog -import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem @@ -101,25 +102,25 @@ fun VaultItemListingContent( LazyColumn( modifier = modifier, ) { - item { - if (showAddTotpBanner) { + if (showAddTotpBanner) { + item { Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenInfoCalloutCard( text = stringResource(id = R.string.add_this_authenticator_key_to_a_login), modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) } } - item { - if (policyDisablesSend) { + if (policyDisablesSend) { + item { Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenInfoCalloutCard( text = stringResource(id = R.string.send_disabled_warning), modifier = Modifier - .padding(horizontal = 16.dp) + .standardHorizontalMargin() .fillMaxWidth(), ) } @@ -127,83 +128,80 @@ fun VaultItemListingContent( if (state.displayCollectionList.isNotEmpty()) { item { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.collections), supportingLabel = state.displayCollectionList.count().toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - item { - Spacer(modifier = Modifier.height(4.dp)) - } - - items(state.displayCollectionList) { collection -> + itemsIndexed(state.displayCollectionList) { index, collection -> BitwardenGroupItem( startIcon = rememberVectorPainter(id = R.drawable.ic_collections), label = collection.name, supportingLabel = collection.count.toString(), onClick = { collectionClick(collection.id) }, showDivider = false, + cardStyle = state + .displayCollectionList + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } if (state.displayFolderList.isNotEmpty()) { item { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.folders), supportingLabel = state.displayFolderList.count().toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - item { - Spacer(modifier = Modifier.height(4.dp)) - } - - items(state.displayFolderList) { folder -> + itemsIndexed(state.displayFolderList) { index, folder -> BitwardenGroupItem( startIcon = rememberVectorPainter(id = R.drawable.ic_folder), label = folder.name, supportingLabel = folder.count.toString(), onClick = { folderClick(folder.id) }, showDivider = false, + cardStyle = state + .displayFolderList + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - } - - if (state.shouldShowDivider) { - item { - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(all = 16.dp), + .standardHorizontalMargin(), ) } } if (state.displayItemList.isNotEmpty()) { item { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.items), supportingLabel = state.displayItemList.size.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(state.displayItemList) { + itemsIndexed(state.displayItemList) { index, it -> BitwardenListItem( startIcon = it.iconData, startIconTestTag = it.iconTestTag, @@ -263,14 +261,12 @@ fun VaultItemListingContent( // Only show options if allowed .filter { !policyDisablesSend } .toPersistentList(), + cardStyle = state + .displayItemList + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() - .padding( - start = 16.dp, - // There is some built-in padding to the menu button that makes up - // the visual difference here. - end = 12.dp, - ), + .standardHorizontalMargin(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt index 6103f061d9..ab9ea46581 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt @@ -14,7 +14,9 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector import com.x8bit.bitwarden.ui.vault.model.VaultCollection @@ -37,7 +39,7 @@ fun VaultMoveToOrganizationContent( ) { if (!showOnlyCollections) { item { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.organization), options = state @@ -52,9 +54,11 @@ fun VaultMoveToOrganizationContent( .first { it.name == selectedString }, ) }, + cardStyle = CardStyle.Full, modifier = Modifier .testTag("OrganizationListDropdown") - .padding(horizontal = 16.dp), + .fillMaxWidth() + .standardHorizontalMargin(), ) } @@ -67,7 +71,8 @@ fun VaultMoveToOrganizationContent( color = BitwardenTheme.colorScheme.text.secondary, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 32.dp), + .standardHorizontalMargin() + .padding(horizontal = 16.dp), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt index d3fa25ed3d..60bcb5f265 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt @@ -3,18 +3,21 @@ package com.x8bit.bitwarden.ui.vault.feature.vault import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.toIconResources import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction @@ -40,6 +43,9 @@ fun VaultContent( LazyColumn( modifier = modifier, ) { + item { + Spacer(modifier = Modifier.height(height = 12.dp)) + } if (state.totpItemsCount > 0) { item { BitwardenListHeaderText( @@ -47,8 +53,10 @@ fun VaultContent( supportingLabel = TOTP_TYPES_COUNT.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { @@ -57,31 +65,31 @@ fun VaultContent( label = stringResource(id = R.string.verification_codes), supportingLabel = state.totpItemsCount.toString(), onClick = vaultHandlers.verificationCodesClick, - showDivider = true, + showDivider = false, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("VerificationCodesFilter") - .padding(16.dp), + .standardHorizontalMargin(), ) } } if (state.favoriteItems.isNotEmpty()) { item { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.favorites), supportingLabel = state.favoriteItems.count().toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - item { - Spacer(modifier = Modifier.height(4.dp)) - } - - items(state.favoriteItems) { favoriteItem -> + itemsIndexed(state.favoriteItems) { index, favoriteItem -> VaultEntryListItem( startIcon = favoriteItem.startIcon, startIconTestTag = favoriteItem.startIconTestTag, @@ -102,39 +110,28 @@ fun VaultContent( vaultHandlers.overflowOptionClick(action) } }, + cardStyle = state + .favoriteItems + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("CipherCell") - .padding( - start = 16.dp, - // There is some built-in padding to the menu button that makes up - // the visual difference here. - end = 12.dp, - ), - ) - } - - item { - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, bottom = 16.dp), + .standardHorizontalMargin(), ) } } item { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.types), supportingLabel = state.itemTypesCount.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) - } - - item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { @@ -145,10 +142,11 @@ fun VaultContent( supportingLabel = state.loginItemsCount.toString(), onClick = vaultHandlers.loginGroupClick, showDivider = false, + cardStyle = CardStyle.Top(dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("LoginFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -160,10 +158,11 @@ fun VaultContent( supportingLabel = state.cardItemsCount.toString(), onClick = vaultHandlers.cardGroupClick, showDivider = false, + cardStyle = CardStyle.Middle(dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("CardFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -175,10 +174,11 @@ fun VaultContent( supportingLabel = state.identityItemsCount.toString(), onClick = vaultHandlers.identityGroupClick, showDivider = false, + cardStyle = CardStyle.Middle(dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("IdentityFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -190,10 +190,15 @@ fun VaultContent( supportingLabel = state.secureNoteItemsCount.toString(), onClick = vaultHandlers.secureNoteGroupClick, showDivider = false, + cardStyle = if (showSshKeys) { + CardStyle.Middle(dividerPadding = 56.dp) + } else { + CardStyle.Bottom + }, modifier = Modifier .fillMaxWidth() .testTag("SecureNoteFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } @@ -206,71 +211,61 @@ fun VaultContent( supportingLabel = state.sshKeyItemsCount.toString(), onClick = vaultHandlers.sshKeyGroupClick, showDivider = false, + cardStyle = CardStyle.Bottom, modifier = Modifier .fillMaxWidth() .testTag("SshKeyFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } if (state.folderItems.isNotEmpty()) { item { - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, bottom = 16.dp), - ) - } - - item { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.folders), supportingLabel = state.folderItems.count().toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - item { - Spacer(modifier = Modifier.height(4.dp)) - } - - items(state.folderItems) { folder -> + itemsIndexed(state.folderItems) { index, folder -> BitwardenGroupItem( startIcon = rememberVectorPainter(id = R.drawable.ic_folder), label = folder.name(), supportingLabel = folder.itemCount.toString(), onClick = { vaultHandlers.folderClick(folder) }, showDivider = false, + cardStyle = state + .folderItems + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("FolderFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } if (state.noFolderItems.isNotEmpty()) { item { - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, bottom = 16.dp), - ) - } - - item { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.folder_none), supportingLabel = state.noFolderItems.count().toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(state.noFolderItems) { noFolderItem -> + itemsIndexed(state.noFolderItems) { index, noFolderItem -> VaultEntryListItem( startIcon = noFolderItem.startIcon, startIconTestTag = noFolderItem.startIconTestTag, @@ -291,68 +286,60 @@ fun VaultContent( vaultHandlers.overflowOptionClick(action) } }, + cardStyle = state + .noFolderItems + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("CipherCell") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } if (state.collectionItems.isNotEmpty()) { item { - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, bottom = 16.dp), - ) - } - - item { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.collections), supportingLabel = state.collectionItems.count().toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(state.collectionItems) { collection -> + itemsIndexed(state.collectionItems) { index, collection -> BitwardenGroupItem( startIcon = rememberVectorPainter(id = R.drawable.ic_collections), label = collection.name, supportingLabel = collection.itemCount.toString(), onClick = { vaultHandlers.collectionClick(collection) }, showDivider = false, + cardStyle = state + .collectionItems + .toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() .testTag("CollectionFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } } item { - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, bottom = 16.dp), - ) - } - - item { + Spacer(modifier = Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.trash), supportingLabel = TRASH_TYPES_COUNT.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) - } - - item { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) } item { @@ -362,13 +349,17 @@ fun VaultContent( supportingLabel = state.trashItemsCount.toString(), onClick = vaultHandlers.trashClick, showDivider = false, + cardStyle = CardStyle.Full, modifier = Modifier .fillMaxWidth() .testTag("TrashFilter") - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } - item { Spacer(modifier = Modifier.height(88.dp)) } + item { + Spacer(modifier = Modifier.height(height = 88.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt index 2b36e83bff..6aecd8450f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -24,6 +25,7 @@ import kotlinx.collections.immutable.toImmutableList * @param onClick The lambda to be invoked when the item is clicked. * @param overflowOptions List of options to display for the item. * @param onOverflowOptionClick The lambda to be invoked when an overflow option is clicked. + * @param cardStyle Indicates the type of card style to be applied. * @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier. * This allows the caller to specify things like padding, size, etc. */ @@ -35,6 +37,7 @@ fun VaultEntryListItem( onClick: () -> Unit, overflowOptions: ImmutableList, onOverflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit, + cardStyle: CardStyle, modifier: Modifier = Modifier, trailingLabelIcons: ImmutableList = persistentListOf(), supportingLabel: String? = null, @@ -55,10 +58,11 @@ fun VaultEntryListItem( ) } .toImmutableList(), + cardStyle = cardStyle, ) } -@Preview(showBackground = true) +@Preview @Composable private fun VaultEntryListItem_preview() { BitwardenTheme { @@ -70,6 +74,7 @@ private fun VaultEntryListItem_preview() { onClick = {}, overflowOptions = persistentListOf(), onOverflowOptionClick = {}, + cardStyle = CardStyle.Full, modifier = Modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt index 780fbe8431..94fa0df268 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeItem.kt @@ -5,9 +5,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -19,9 +20,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.cardBackground +import com.x8bit.bitwarden.ui.platform.base.util.cardPadding import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.icon.BitwardenIcon import com.x8bit.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -36,6 +40,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param startIcon The leading icon for the item. * @param onCopyClick The lambda function to be invoked when the copy button is clicked. * @param onItemClick The lambda function to be invoked when the item is clicked. + * @param cardStyle Indicates the type of card style to be applied. * @param modifier The modifier for the item. * @param supportingLabel The supporting label for the item. */ @@ -50,11 +55,14 @@ fun VaultVerificationCodeItem( startIcon: IconData, onCopyClick: () -> Unit, onItemClick: () -> Unit, + cardStyle: CardStyle?, modifier: Modifier = Modifier, supportingLabel: String? = null, ) { Row( - modifier = Modifier + modifier = modifier + .defaultMinSize(minHeight = 60.dp) + .cardBackground(cardStyle = cardStyle) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple( @@ -62,11 +70,8 @@ fun VaultVerificationCodeItem( ), onClick = onItemClick, ) - .defaultMinSize(minHeight = 72.dp) - .padding(vertical = 8.dp) - .then(modifier), + .cardPadding(cardStyle = cardStyle, start = 16.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), ) { BitwardenIcon( iconData = startIcon, @@ -75,6 +80,8 @@ fun VaultVerificationCodeItem( modifier = Modifier.size(24.dp), ) + Spacer(modifier = Modifier.width(width = 16.dp)) + Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.SpaceEvenly, @@ -102,27 +109,30 @@ fun VaultVerificationCodeItem( BitwardenCircularCountdownIndicator( timeLeftSeconds = timeLeftSeconds, periodSeconds = periodSeconds, + modifier = Modifier.size(size = 56.dp), ) if (!hideAuthCode) { Text( text = authCode.chunked(3).joinToString(" "), - style = BitwardenTheme.typography.bodyLarge, + style = BitwardenTheme.typography.sensitiveInfoSmall, color = BitwardenTheme.colorScheme.text.primary, ) + Spacer(modifier = Modifier.width(width = 16.dp)) + BitwardenStandardIconButton( vectorIconRes = R.drawable.ic_copy, contentDescription = stringResource(id = R.string.copy), onClick = onCopyClick, - contentColor = BitwardenTheme.colorScheme.icon.secondary, + contentColor = BitwardenTheme.colorScheme.icon.primary, ) } } } @Suppress("MagicNumber") -@Preview(showBackground = true) +@Preview @Composable private fun VerificationCodeItem_preview() { BitwardenTheme { @@ -136,7 +146,7 @@ private fun VerificationCodeItem_preview() { periodSeconds = 30, onCopyClick = {}, onItemClick = {}, - modifier = Modifier.padding(horizontal = 16.dp), + cardStyle = CardStyle.Full, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt index ae3b690e3e..59869db549 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt @@ -1,10 +1,13 @@ package com.x8bit.bitwarden.ui.vault.feature.verificationcode +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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -19,6 +22,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin +import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenSearchActionItem @@ -155,16 +160,19 @@ private fun VerificationCodeContent( modifier = modifier, ) { item { + Spacer(modifier = Modifier.height(height = 12.dp)) BitwardenListHeaderText( label = stringResource(id = R.string.items), supportingLabel = items.size.toString(), modifier = Modifier .fillMaxWidth() + .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(height = 8.dp)) } - items(items) { + itemsIndexed(items) { index, it -> VaultVerificationCodeItem( startIcon = it.startIcon, label = it.label, @@ -177,10 +185,16 @@ private fun VerificationCodeContent( onItemClick = { itemClick(it.id) }, + cardStyle = items.toListItemCardStyle(index = index, dividerPadding = 56.dp), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .standardHorizontalMargin(), ) } + + item { + Spacer(modifier = Modifier.height(height = 16.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb7c068a93..ac599d2b78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1112,4 +1112,6 @@ Do you want to switch to this account? Review flow launched! Copy private key You can change your account email on the Bitwarden web app. + Login Credentials + Autofill Options diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreenTest.kt index 9e9ddd3c82..56fad26a2f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreenTest.kt @@ -112,22 +112,13 @@ class EnvironmentScreenTest : BaseComposeTest() { .onNodeWithText("Server URL") // Click to focus to see placeholder .performClick() - .assertTextEquals( - "Server URL", - "Specify the base URL of your on-premise hosted Bitwarden installation.", - "ex. https://bitwarden.company.com", - "", - ) + .assertTextEquals("Server URL", "ex. https://bitwarden.company.com", "") mutableStateFlow.update { it.copy(serverUrl = "server-url") } composeTestRule .onNodeWithText("Server URL") - .assertTextEquals( - "Server URL", - "Specify the base URL of your on-premise hosted Bitwarden installation.", - "server-url", - ) + .assertTextEquals("Server URL", "server-url") } @Test @@ -223,21 +214,13 @@ class EnvironmentScreenTest : BaseComposeTest() { fun `icons server URL should change according to the state`() { composeTestRule .onNodeWithText("Icons server URL") - .assertTextEquals( - "Icons server URL", - "For advanced users. You can specify the base URL of each service independently.", - "", - ) + .assertTextEquals("Icons server URL", "") mutableStateFlow.update { it.copy(iconsServerUrl = "icons-url") } composeTestRule .onNodeWithText("Icons server URL") - .assertTextEquals( - "Icons server URL", - "For advanced users. You can specify the base URL of each service independently.", - "icons-url", - ) + .assertTextEquals("Icons server URL", "icons-url") } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt index c708281ebd..f9a5747c42 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest @@ -45,7 +46,7 @@ class OtherScreenTest : BaseComposeTest() { @Test fun `on allow screen capture confirm should send AllowScreenCaptureToggle`() { - composeTestRule.onNodeWithText("Allow screen capture").performClick() + composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick() composeTestRule.onNodeWithText("Yes").performClick() composeTestRule.assertNoDialogExists() @@ -54,7 +55,7 @@ class OtherScreenTest : BaseComposeTest() { @Test fun `on allow screen capture cancel should dismiss dialog`() { - composeTestRule.onNodeWithText("Allow screen capture").performClick() + composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick() composeTestRule .onAllNodesWithText("Cancel") .filterToOne(hasAnyAncestor(isDialog())) @@ -64,7 +65,7 @@ class OtherScreenTest : BaseComposeTest() { @Test fun `on allow screen capture row click should display confirm enable screen capture dialog`() { - composeTestRule.onNodeWithText("Allow screen capture").performClick() + composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick() composeTestRule .onAllNodesWithText("Allow screen capture") .filterToOne(hasAnyAncestor(isDialog())) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 0d83e1f8fe..c70d39c797 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -1366,11 +1366,11 @@ class GeneratorScreenTest : BaseComposeTest() { //region Username Type Tests - @Suppress("MaxLineLength") @Test fun `in Username state, clicking the tooltip icon should send the TooltipClick action`() { updateState(DEFAULT_STATE.copy(selectedType = GeneratorState.MainType.Username())) + @Suppress("MaxLineLength") composeTestRule .onNodeWithContentDescription( label = "Plus addressed email. Username type. Use your email provider's subaddress capabilities", diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt index 268a7a0554..9c5e12bd4b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreenTest.kt @@ -72,7 +72,7 @@ class PasswordHistoryScreenTest : BaseComposeTest() { @Test fun `navigation icon click should trigger navigate back`() { - composeTestRule.onNodeWithContentDescription("Close").performClick() + composeTestRule.onNodeWithContentDescription("Back").performClick() verify { viewModel.trySendAction( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index 407029d6c8..f1cdbd5992 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -333,11 +333,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `name input should change according to the state`() { composeTestRule .onNodeWithText("Name") - .assertTextEquals( - "Name", - "A friendly name to describe this Send.", - "", - ) + .assertTextEquals("Name", "") mutableStateFlow.update { it.copy( @@ -348,11 +344,7 @@ class AddSendScreenTest : BaseComposeTest() { } composeTestRule .onNodeWithText("Name") - .assertTextEquals( - "Name", - "A friendly name to describe this Send.", - "input", - ) + .assertTextEquals("Name", "input") } @Test @@ -484,11 +476,7 @@ class AddSendScreenTest : BaseComposeTest() { composeTestRule .onAllNodesWithText("Text") .filterToOne(hasSetTextAction()) - .assertTextEquals( - "Text", - "The text you want to send.", - "", - ) + .assertTextEquals("Text", "") mutableStateFlow.update { it.copy( @@ -503,11 +491,7 @@ class AddSendScreenTest : BaseComposeTest() { composeTestRule .onAllNodesWithText("Text") .filterToOne(hasSetTextAction()) - .assertTextEquals( - "Text", - "The text you want to send.", - "input", - ) + .assertTextEquals("Text", "input") } @Test @@ -563,7 +547,7 @@ class AddSendScreenTest : BaseComposeTest() { .onNodeWithText("Deactivate this Send", substring = true) .assertDoesNotExist() composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule @@ -593,7 +577,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `max access count decrement should be disabled when max access count is null`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -614,7 +598,7 @@ class AddSendScreenTest : BaseComposeTest() { } // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -629,7 +613,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `on max access count increment should send MaxAccessCountChange`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -644,7 +628,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `on password input change should send PasswordChange`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -658,16 +642,12 @@ class AddSendScreenTest : BaseComposeTest() { fun `password input should change according to the state`() { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule .onNodeWithText("New password") - .assertTextEquals( - "New password", - "Optionally require a password for users to access this Send.", - "", - ) + .assertTextEquals("New password", "") mutableStateFlow.update { it.copy( @@ -678,18 +658,14 @@ class AddSendScreenTest : BaseComposeTest() { } composeTestRule .onNodeWithText("New password") - .assertTextEquals( - "New password", - "Optionally require a password for users to access this Send.", - "•••••", - ) + .assertTextEquals("New password", "•••••") } @Test fun `on notes input change should send NoteChange`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -703,16 +679,12 @@ class AddSendScreenTest : BaseComposeTest() { fun `note input should change according to the state`() { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule .onNodeWithText("Notes") - .assertTextEquals( - "Notes", - "Private notes about this Send.", - "", - ) + .assertTextEquals("Notes", "") mutableStateFlow.update { it.copy( @@ -723,18 +695,14 @@ class AddSendScreenTest : BaseComposeTest() { } composeTestRule .onNodeWithText("Notes") - .assertTextEquals( - "Notes", - "Private notes about this Send.", - "input", - ) + .assertTextEquals("Notes", "input") } @Test fun `on hide email toggle should send HideMyEmailToggle`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -749,7 +717,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `hide email toggle should change according to the state`() { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule @@ -772,7 +740,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `hide email toggle should be disabled according to state`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -815,7 +783,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `on deactivate this send toggle should send DeactivateThisSendToggle`() = runTest { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() @@ -830,7 +798,7 @@ class AddSendScreenTest : BaseComposeTest() { fun `deactivate send toggle should change according to the state`() { // Expand options section: composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule @@ -856,7 +824,7 @@ class AddSendScreenTest : BaseComposeTest() { } composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule @@ -894,7 +862,7 @@ class AddSendScreenTest : BaseComposeTest() { } composeTestRule - .onNodeWithText("Options") + .onNodeWithText("Additional options") .performScrollTo() .performClick() composeTestRule diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 525a2013d4..202c5dc621 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -1261,15 +1261,15 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `on login copy notes field click should send CopyNotesClick`() { - mutableStateFlow.update { currentState -> currentState.copy( viewState = DEFAULT_LOGIN_VIEW_STATE, ) } - composeTestRule.onNodeWithTextAfterScroll("Lots of notes") + // We scroll to custom fields, which is right after notes to avoid clicking on the FAB + composeTestRule.onNodeWithTextAfterScroll("CUSTOM FIELDS") composeTestRule - .onNodeWithTag("CipherNotesCopyButton") + .onNodeWithContentDescription("Copy note") .performClick() verify { @@ -1638,6 +1638,7 @@ class VaultItemScreenTest : BaseComposeTest() { ) } + composeTestRule.onNodeWithTextAfterScroll("Verification code (TOTP)") // There are 2 because of the pull-to-refresh composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2) @@ -1654,11 +1655,12 @@ class VaultItemScreenTest : BaseComposeTest() { ) } + composeTestRule.onNodeWithTextAfterScroll("Verification code (TOTP)") // There are 2 because of the pull-to-refresh composeTestRule.onAllNodes(isProgressBar).assertCountEquals(2) composeTestRule - .onNodeWithContentDescription("Copy TOTP") + .onNodeWithContentDescriptionAfterScroll("Copy TOTP") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -1671,6 +1673,7 @@ class VaultItemScreenTest : BaseComposeTest() { ) } + composeTestRule.onNodeWithTextAfterScroll("Verification code (TOTP)") // Only pull-to-refresh remains composeTestRule.onAllNodes(isProgressBar).assertCountEquals(1) @@ -1688,7 +1691,7 @@ class VaultItemScreenTest : BaseComposeTest() { } composeTestRule - .onNodeWithContentDescription("Copy TOTP") + .onNodeWithContentDescriptionAfterScroll("Copy TOTP") .performClick() verify { @@ -1913,7 +1916,7 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `in login state, uris should be displayed according to state`() { mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) } - composeTestRule.onNodeWithTextAfterScroll("URIS").assertIsDisplayed() + composeTestRule.onNodeWithTextAfterScroll("AUTOFILL OPTIONS").assertIsDisplayed() composeTestRule.onNodeWithTextAfterScroll("URI").assertIsDisplayed() composeTestRule.onNodeWithTextAfterScroll("www.example.com").assertIsDisplayed() @@ -1921,7 +1924,7 @@ class VaultItemScreenTest : BaseComposeTest() { updateLoginType(currentState) { copy(uris = emptyList()) } } - composeTestRule.assertScrollableNodeDoesNotExist("URIS") + composeTestRule.assertScrollableNodeDoesNotExist("AUTOFILL OPTIONS") composeTestRule.assertScrollableNodeDoesNotExist("URI") composeTestRule.assertScrollableNodeDoesNotExist("www.example.com") } @@ -2091,9 +2094,9 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `in identity state, on copy username field click should send CopyUsernameClick`() { - val username = "the username" mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } - composeTestRule.onNodeWithTextAfterScroll(username) + // We scroll to company, which is right after the username to avoid clicking on the FAB + composeTestRule.onNodeWithTextAfterScroll("Company") composeTestRule .onNodeWithTag("IdentityCopyUsernameButton") @@ -2154,10 +2157,9 @@ class VaultItemScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in identity state, on copy license number field click should send CopyLicenseNumberClick`() { - val licenseNumber = "the license number" mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } - composeTestRule.onNodeWithTextAfterScroll(licenseNumber) - + // We scroll to email, which is right after the license number to avoid clicking on the FAB + composeTestRule.onNodeWithTextAfterScroll("Email") composeTestRule .onNodeWithTag("IdentityCopyLicenseNumberButton") .performClick() @@ -2198,9 +2200,9 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `in identity state, on copy address field click should send CopyAddressClick`() { - val address = "the address" mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } - composeTestRule.onNodeWithTextAfterScroll(address) + // We scroll to notes, which is right after the address to avoid clicking on the FAB + composeTestRule.onNodeWithTextAfterScroll("Notes") composeTestRule .onNodeWithTag("IdentityCopyAddressButton") @@ -2548,8 +2550,10 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `in ssh key state, on copy private key click should send CopyPrivateKeyClick`() { mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) } + // We scroll to fingerprint, which is right after the private to avoid clicking on the FAB + composeTestRule.onNodeWithTextAfterScroll("Fingerprint") composeTestRule - .onNodeWithContentDescriptionAfterScroll("Copy private key") + .onNodeWithContentDescription("Copy private key") .performClick() verify(exactly = 1) { @@ -2567,6 +2571,8 @@ class VaultItemScreenTest : BaseComposeTest() { @Test fun `in ssh key state, on copy fingerprint click should send CopyFingerprintClick`() { mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) } + // We scroll to notes, which is right after the fingerprint to avoid clicking on the FAB + composeTestRule.onNodeWithTextAfterScroll("Notes") composeTestRule .onNodeWithContentDescription("Copy fingerprint") .performClick() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index f3d31f8891..d9eed7f595 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -894,7 +894,7 @@ class VaultScreenTest : BaseComposeTest() { .assertTextEquals(collectionsHeader) .assertIsDisplayed() composeTestRule - .onNodeWithText(collectionName) + .onNodeWithTextAfterScroll(collectionName) .assertTextEquals(collectionName, collectionCount.toString()) }