mirror of
https://github.com/bitwarden/android.git
synced 2024-11-25 10:56:03 +03:00
Add BitwardenStepper component (#226)
This commit is contained in:
parent
0185456dca
commit
bcffbe6fce
8 changed files with 323 additions and 144 deletions
|
@ -0,0 +1,73 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenStepper(
|
||||||
|
label: String,
|
||||||
|
value: Int?,
|
||||||
|
onValueChange: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
range: ClosedRange<Int> = 1..Int.MAX_VALUE,
|
||||||
|
isIncrementEnabled: Boolean = true,
|
||||||
|
isDecrementEnabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
val clampedValue = value?.coerceIn(range)
|
||||||
|
if (clampedValue != value && clampedValue != null) {
|
||||||
|
onValueChange(clampedValue)
|
||||||
|
}
|
||||||
|
BitwardenReadOnlyTextFieldWithActions(
|
||||||
|
label = label,
|
||||||
|
// we use a space instead of empty string to make sure label is shown small and above
|
||||||
|
// the input
|
||||||
|
value = clampedValue
|
||||||
|
?.toString()
|
||||||
|
?: " ",
|
||||||
|
actions = {
|
||||||
|
BitwardenIconButtonWithResource(
|
||||||
|
iconRes = IconResource(
|
||||||
|
iconPainter = painterResource(id = R.drawable.ic_minus),
|
||||||
|
contentDescription = "\u2212",
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
val decrementedValue = ((value ?: 0) - 1).coerceIn(range)
|
||||||
|
if (decrementedValue != value) {
|
||||||
|
onValueChange(decrementedValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled = isDecrementEnabled,
|
||||||
|
)
|
||||||
|
BitwardenIconButtonWithResource(
|
||||||
|
iconRes = IconResource(
|
||||||
|
iconPainter = painterResource(id = R.drawable.ic_plus),
|
||||||
|
contentDescription = "+",
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
val incrementedValue = ((value ?: 0) + 1).coerceIn(range)
|
||||||
|
if (incrementedValue != value) {
|
||||||
|
onValueChange(incrementedValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled = isIncrementEnabled,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
|
@ -50,10 +50,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenReadOnlyTextFieldWithActions
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenReadOnlyTextFieldWithActions
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MAX_NUMBER_OF_WORDS
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MAX
|
||||||
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MAX
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_LENGTH_SLIDER_MIN
|
||||||
|
|
||||||
|
@ -472,29 +477,11 @@ private fun PasswordMinNumbersCounterItem(
|
||||||
minNumbers: Int,
|
minNumbers: Int,
|
||||||
onPasswordMinNumbersCounterChange: (Int) -> Unit,
|
onPasswordMinNumbersCounterChange: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
BitwardenReadOnlyTextFieldWithActions(
|
BitwardenStepper(
|
||||||
label = stringResource(id = R.string.min_numbers),
|
label = stringResource(id = R.string.min_numbers),
|
||||||
value = minNumbers.toString(),
|
value = minNumbers,
|
||||||
actions = {
|
range = PASSWORD_COUNTER_MIN..PASSWORD_COUNTER_MAX,
|
||||||
BitwardenIconButtonWithResource(
|
onValueChange = onPasswordMinNumbersCounterChange,
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_minus),
|
|
||||||
contentDescription = "\u2212",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onPasswordMinNumbersCounterChange(minNumbers - 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
BitwardenIconButtonWithResource(
|
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_plus),
|
|
||||||
contentDescription = "+",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onPasswordMinNumbersCounterChange(minNumbers + 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -504,29 +491,11 @@ private fun PasswordMinSpecialCharactersCounterItem(
|
||||||
minSpecial: Int,
|
minSpecial: Int,
|
||||||
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
|
onPasswordMinSpecialCharactersChange: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
BitwardenReadOnlyTextFieldWithActions(
|
BitwardenStepper(
|
||||||
label = stringResource(id = R.string.min_special),
|
label = stringResource(id = R.string.min_special),
|
||||||
value = minSpecial.toString(),
|
value = minSpecial,
|
||||||
actions = {
|
range = PASSWORD_COUNTER_MIN..PASSWORD_COUNTER_MAX,
|
||||||
BitwardenIconButtonWithResource(
|
onValueChange = onPasswordMinSpecialCharactersChange,
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_minus),
|
|
||||||
contentDescription = "\u2212",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onPasswordMinSpecialCharactersChange(minSpecial - 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
BitwardenIconButtonWithResource(
|
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_plus),
|
|
||||||
contentDescription = "+",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onPasswordMinSpecialCharactersChange(minSpecial + 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -586,29 +555,11 @@ private fun PassphraseNumWordsCounterItem(
|
||||||
numWords: Int,
|
numWords: Int,
|
||||||
onPassphraseNumWordsCounterChange: (Int) -> Unit,
|
onPassphraseNumWordsCounterChange: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
BitwardenReadOnlyTextFieldWithActions(
|
BitwardenStepper(
|
||||||
label = stringResource(id = R.string.number_of_words),
|
label = stringResource(id = R.string.number_of_words),
|
||||||
value = numWords.toString(),
|
value = numWords,
|
||||||
actions = {
|
range = PASSPHRASE_MIN_NUMBER_OF_WORDS..PASSPHRASE_MAX_NUMBER_OF_WORDS,
|
||||||
BitwardenIconButtonWithResource(
|
onValueChange = onPassphraseNumWordsCounterChange,
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_minus),
|
|
||||||
contentDescription = "\u2212",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onPassphraseNumWordsCounterChange(numWords - 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
BitwardenIconButtonWithResource(
|
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_plus),
|
|
||||||
contentDescription = "+",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onPassphraseNumWordsCounterChange(numWords + 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,7 @@ import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MAX_NUMBER_OF_WORDS
|
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS
|
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MAX
|
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password.Companion.PASSWORD_COUNTER_MIN
|
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeTypeOption
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeTypeOption
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AnonAddy
|
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AnonAddy
|
||||||
|
@ -24,8 +20,6 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.lang.Integer.max
|
|
||||||
import java.lang.Integer.min
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val KEY_STATE = "state"
|
private const val KEY_STATE = "state"
|
||||||
|
@ -266,24 +260,16 @@ class GeneratorViewModel @Inject constructor(
|
||||||
private fun handleMinNumbersChange(
|
private fun handleMinNumbersChange(
|
||||||
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
|
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinNumbersCounterChange,
|
||||||
) {
|
) {
|
||||||
val adjustedMinNumbers = action
|
|
||||||
.minNumbers
|
|
||||||
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
|
|
||||||
|
|
||||||
updatePasswordType { currentPasswordType ->
|
updatePasswordType { currentPasswordType ->
|
||||||
currentPasswordType.copy(minNumbers = adjustedMinNumbers)
|
currentPasswordType.copy(minNumbers = action.minNumbers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMinSpecialChange(
|
private fun handleMinSpecialChange(
|
||||||
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
|
action: GeneratorAction.MainType.Passcode.PasscodeType.Password.MinSpecialCharactersChange,
|
||||||
) {
|
) {
|
||||||
val adjustedMinSpecial = action
|
|
||||||
.minSpecial
|
|
||||||
.coerceIn(PASSWORD_COUNTER_MIN, PASSWORD_COUNTER_MAX)
|
|
||||||
|
|
||||||
updatePasswordType { currentPasswordType ->
|
updatePasswordType { currentPasswordType ->
|
||||||
currentPasswordType.copy(minSpecial = adjustedMinSpecial)
|
currentPasswordType.copy(minSpecial = action.minSpecial)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,11 +338,7 @@ class GeneratorViewModel @Inject constructor(
|
||||||
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange,
|
action: GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange,
|
||||||
) {
|
) {
|
||||||
updatePassphraseType { passphraseType ->
|
updatePassphraseType { passphraseType ->
|
||||||
val newNumWords = max(
|
passphraseType.copy(numWords = action.numWords)
|
||||||
PASSPHRASE_MIN_NUMBER_OF_WORDS,
|
|
||||||
min(PASSPHRASE_MAX_NUMBER_OF_WORDS, action.numWords),
|
|
||||||
)
|
|
||||||
passphraseType.copy(numWords = newNumWords)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,19 +43,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
|
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenReadOnlyTextFieldWithActions
|
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||||
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
|
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.NewSendAction.MaxAccessCountChange
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays new send UX.
|
* Displays new send UX.
|
||||||
|
@ -211,11 +208,8 @@ fun NewSendScreen(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
NewSendOptions(
|
NewSendOptions(
|
||||||
state = state,
|
state = state,
|
||||||
onIncrementMaxAccessCountClick = remember(viewModel) {
|
onMaxAccessCountChange = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(MaxAccessCountChange(it)) }
|
{ viewModel.trySendAction(NewSendAction.MaxAccessCountChange(it)) }
|
||||||
},
|
|
||||||
onDecrementMaxAccessCountClick = remember(viewModel) {
|
|
||||||
{ viewModel.trySendAction(MaxAccessCountChange(it)) }
|
|
||||||
},
|
},
|
||||||
onPasswordChange = remember(viewModel) {
|
onPasswordChange = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(NewSendAction.PasswordChange(it)) }
|
{ viewModel.trySendAction(NewSendAction.PasswordChange(it)) }
|
||||||
|
@ -238,8 +232,7 @@ fun NewSendScreen(
|
||||||
* Displays a collapsable set of new send options.
|
* Displays a collapsable set of new send options.
|
||||||
*
|
*
|
||||||
* @param state state.
|
* @param state state.
|
||||||
* @param onIncrementMaxAccessCountClick called when increment max access count is clicked.
|
* @param onMaxAccessCountChange called when max access count changes.
|
||||||
* @param onDecrementMaxAccessCountClick called when decrement max access count is clicked.
|
|
||||||
* @param onPasswordChange called when the password changes.
|
* @param onPasswordChange called when the password changes.
|
||||||
* @param onNoteChange called when the notes changes.
|
* @param onNoteChange called when the notes changes.
|
||||||
* @param onHideEmailChecked called when hide email is checked.
|
* @param onHideEmailChecked called when hide email is checked.
|
||||||
|
@ -249,8 +242,7 @@ fun NewSendScreen(
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewSendOptions(
|
private fun NewSendOptions(
|
||||||
state: NewSendState,
|
state: NewSendState,
|
||||||
onIncrementMaxAccessCountClick: (Int) -> Unit,
|
onMaxAccessCountChange: (Int) -> Unit,
|
||||||
onDecrementMaxAccessCountClick: (Int) -> Unit,
|
|
||||||
onPasswordChange: (String) -> Unit,
|
onPasswordChange: (String) -> Unit,
|
||||||
onNoteChange: (String) -> Unit,
|
onNoteChange: (String) -> Unit,
|
||||||
onHideEmailChecked: (Boolean) -> Unit,
|
onHideEmailChecked: (Boolean) -> Unit,
|
||||||
|
@ -300,33 +292,13 @@ private fun NewSendOptions(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
BitwardenReadOnlyTextFieldWithActions(
|
BitwardenStepper(
|
||||||
label = stringResource(R.string.maximum_access_count),
|
label = stringResource(id = R.string.maximum_access_count),
|
||||||
// we use a space instead of empty string to make sure label is shown small and
|
value = state.maxAccessCount,
|
||||||
// above the input
|
onValueChange = onMaxAccessCountChange,
|
||||||
value = state.maxAccessCount?.toString() ?: " ",
|
isDecrementEnabled = state.maxAccessCount != null,
|
||||||
actions = {
|
modifier = Modifier
|
||||||
BitwardenIconButtonWithResource(
|
.padding(horizontal = 16.dp),
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_minus),
|
|
||||||
contentDescription = "\u2212",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onIncrementMaxAccessCountClick.invoke((state.maxAccessCount ?: 0) - 1)
|
|
||||||
},
|
|
||||||
isEnabled = state.maxAccessCount != null,
|
|
||||||
)
|
|
||||||
BitwardenIconButtonWithResource(
|
|
||||||
iconRes = IconResource(
|
|
||||||
iconPainter = painterResource(id = R.drawable.ic_plus),
|
|
||||||
contentDescription = "+",
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onDecrementMaxAccessCountClick.invoke((state.maxAccessCount ?: 0) + 1)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
private const val KEY_STATE = "state"
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
|
@ -127,7 +126,7 @@ class NewSendViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun handleMaxAccessCountChange(action: NewSendAction.MaxAccessCountChange) {
|
private fun handleMaxAccessCountChange(action: NewSendAction.MaxAccessCountChange) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(maxAccessCount = max(1, action.newValue))
|
it.copy(maxAccessCount = action.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,9 +238,9 @@ sealed class NewSendAction {
|
||||||
data class HideByDefaultToggle(val isChecked: Boolean) : NewSendAction()
|
data class HideByDefaultToggle(val isChecked: Boolean) : NewSendAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User incremented or decremented the max access count.
|
* User incremented the max access count.
|
||||||
*/
|
*/
|
||||||
data class MaxAccessCountChange(val newValue: Int) : NewSendAction()
|
data class MaxAccessCountChange(val value: Int) : NewSendAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User toggled the "hide my email" toggle.
|
* User toggled the "hide my email" toggle.
|
||||||
|
|
|
@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class GeneratorScreenTest : BaseComposeTest() {
|
class GeneratorScreenTest : BaseComposeTest() {
|
||||||
private val mutableStateFlow = MutableStateFlow(
|
private val mutableStateFlow = MutableStateFlow(
|
||||||
GeneratorState(
|
GeneratorState(
|
||||||
|
@ -409,6 +410,70 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `in Passcode_Password state, decrementing the minimum numbers counter below 0 should do nothing`() {
|
||||||
|
val initialMinNumbers = 0
|
||||||
|
updateState(
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = "Placeholder",
|
||||||
|
selectedType = GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode(
|
||||||
|
GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode
|
||||||
|
.PasscodeType
|
||||||
|
.Password(minNumbers = initialMinNumbers),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `in Passcode_Password state, incrementing the minimum numbers counter above 5 should do nothing`() {
|
||||||
|
val initialMinNumbers = 5
|
||||||
|
updateState(
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = "Placeholder",
|
||||||
|
selectedType = GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode(
|
||||||
|
GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode
|
||||||
|
.PasscodeType
|
||||||
|
.Password(minNumbers = initialMinNumbers),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Minimum numbers, $initialMinNumbers")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
|
fun `in Passcode_Password state, decrementing the minimum special characters counter should send MinSpecialCharactersChange action`() {
|
||||||
|
@ -485,6 +550,70 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `in Passcode_Password state, decrementing the minimum special characters below 0 should do nothing`() {
|
||||||
|
val initialSpecialChars = 0
|
||||||
|
updateState(
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = "Placeholder",
|
||||||
|
selectedType = GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode(
|
||||||
|
GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode
|
||||||
|
.PasscodeType
|
||||||
|
.Password(minSpecial = initialSpecialChars),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `in Passcode_Password state, decrementing the minimum special characters above 5 should do nothing`() {
|
||||||
|
val initialSpecialChars = 5
|
||||||
|
updateState(
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = "Placeholder",
|
||||||
|
selectedType = GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode(
|
||||||
|
GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode
|
||||||
|
.PasscodeType
|
||||||
|
.Password(minSpecial = initialSpecialChars),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Minimum special, $initialSpecialChars")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
|
fun `in Passcode_Password state, toggling the use avoid ambiguous characters toggle should send ToggleSpecialCharactersChange action`() {
|
||||||
|
@ -517,7 +646,7 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
|
fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() {
|
||||||
val initialNumWords = 3
|
val initialNumWords = 4
|
||||||
updateState(
|
updateState(
|
||||||
GeneratorState(
|
GeneratorState(
|
||||||
generatedText = "Placeholder",
|
generatedText = "Placeholder",
|
||||||
|
@ -528,7 +657,7 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
.MainType
|
.MainType
|
||||||
.Passcode
|
.Passcode
|
||||||
.PasscodeType
|
.PasscodeType
|
||||||
.Passphrase(),
|
.Passphrase(numWords = initialNumWords),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -539,7 +668,7 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
// Unicode for "minus" used for content description
|
// Unicode for "minus" used for content description
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithContentDescription("Number of words, 3")
|
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
||||||
.onChildren()
|
.onChildren()
|
||||||
.filterToOne(hasContentDescription("\u2212"))
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -554,6 +683,70 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in Passcode_Passphrase state, decrementing number of words under 3 should do nothing`() {
|
||||||
|
val initialNumWords = 3
|
||||||
|
updateState(
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = "Placeholder",
|
||||||
|
selectedType = GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode(
|
||||||
|
GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode
|
||||||
|
.PasscodeType
|
||||||
|
.Passphrase(numWords = initialNumWords),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicode for "minus" used for content description
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("\u2212"))
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in Passcode_Passphrase state, incrementing number of words over 20 should do nothing`() {
|
||||||
|
val initialNumWords = 20
|
||||||
|
updateState(
|
||||||
|
GeneratorState(
|
||||||
|
generatedText = "Placeholder",
|
||||||
|
selectedType = GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode(
|
||||||
|
GeneratorState
|
||||||
|
.MainType
|
||||||
|
.Passcode
|
||||||
|
.PasscodeType
|
||||||
|
.Passphrase(numWords = initialNumWords),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule.setContent {
|
||||||
|
GeneratorScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicode for "minus" used for content description
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Number of words, $initialNumWords")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("+"))
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `in Passcode_Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() {
|
fun `in Passcode_Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() {
|
||||||
|
|
|
@ -248,6 +248,25 @@ class NewSendScreenTest : BaseComposeTest() {
|
||||||
verify { viewModel.trySendAction(NewSendAction.MaxAccessCountChange(2)) }
|
verify { viewModel.trySendAction(NewSendAction.MaxAccessCountChange(2)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `max access count decrement when set to 1 should do nothing`() =
|
||||||
|
runTest {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(maxAccessCount = 1)
|
||||||
|
}
|
||||||
|
// Expand options section:
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Options")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("\u2212")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on max access count increment should send MaxAccessCountChange`() = runTest {
|
fun `on max access count increment should send MaxAccessCountChange`() = runTest {
|
||||||
// Expand options section:
|
// Expand options section:
|
||||||
|
|
|
@ -87,16 +87,6 @@ class NewSendViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `MaxAccessCountChang below 1 should keep maxAccessCount at 1`() = runTest {
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
viewModel.stateFlow.test {
|
|
||||||
assertEquals(DEFAULT_STATE, awaitItem())
|
|
||||||
viewModel.trySendAction(NewSendAction.MaxAccessCountChange(0))
|
|
||||||
assertEquals(DEFAULT_STATE.copy(maxAccessCount = 1), awaitItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `TextChange should update text input`() = runTest {
|
fun `TextChange should update text input`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
Loading…
Reference in a new issue