mirror of
https://github.com/bitwarden/android.git
synced 2026-06-08 23:16:33 -05:00
Refactor AddSendViewModel to support loading and error states. (#524)
This commit is contained in:
committed by
Álison Fernandes
parent
1e8d603b61
commit
978e72899b
@@ -0,0 +1,281 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.BitwardenFilledTonalButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.SendDeletionDateChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.SendExpirationDateChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
|
||||
|
||||
/**
|
||||
* Content view for the [AddSendScreen].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun AddSendContent(
|
||||
state: AddSendState.ViewState.Content,
|
||||
addSendHandlers: AddSendHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
BitwardenTextField(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.name),
|
||||
hint = stringResource(id = R.string.name_info),
|
||||
value = state.common.name,
|
||||
onValueChange = addSendHandlers.onNamChange,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.type),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenSegmentedButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
options = listOf(
|
||||
SegmentedButtonState(
|
||||
text = stringResource(id = R.string.file),
|
||||
onClick = addSendHandlers.onFileTypeSelect,
|
||||
isChecked = state.selectedType is AddSendState.ViewState.Content.SendType.File,
|
||||
),
|
||||
SegmentedButtonState(
|
||||
text = stringResource(id = R.string.text),
|
||||
onClick = addSendHandlers.onTextTypeSelect,
|
||||
isChecked = state.selectedType is AddSendState.ViewState.Content.SendType.Text,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
when (val type = state.selectedType) {
|
||||
is AddSendState.ViewState.Content.SendType.File -> {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.file),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = R.string.no_file_chosen),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(id = R.string.choose_file),
|
||||
onClick = addSendHandlers.onChooseFileCLick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.max_file_size),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.type_file_info),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
is AddSendState.ViewState.Content.SendType.Text -> {
|
||||
BitwardenTextField(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.text),
|
||||
hint = stringResource(id = R.string.type_text_info),
|
||||
value = type.input,
|
||||
onValueChange = addSendHandlers.onTextChange,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.hide_text_by_default),
|
||||
isChecked = type.isHideByDefaultChecked,
|
||||
onCheckedChange = addSendHandlers.onIsHideByDefaultToggle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AddSendOptions(
|
||||
state = state,
|
||||
onMaxAccessCountChange = addSendHandlers.onMaxAccessCountChange,
|
||||
onPasswordChange = addSendHandlers.onPasswordChange,
|
||||
onNoteChange = addSendHandlers.onNoteChange,
|
||||
onHideEmailChecked = addSendHandlers.onHideEmailToggle,
|
||||
onDeactivateSendChecked = addSendHandlers.onDeactivateSendToggle,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a collapsable set of new send options.
|
||||
*
|
||||
* @param state The content state.
|
||||
* @param onMaxAccessCountChange called when max access count changes.
|
||||
* @param onPasswordChange called when the password changes.
|
||||
* @param onNoteChange called when the notes changes.
|
||||
* @param onHideEmailChecked called when hide email is checked.
|
||||
* @param onDeactivateSendChecked called when deactivate send is checked.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun AddSendOptions(
|
||||
state: AddSendState.ViewState.Content,
|
||||
onMaxAccessCountChange: (Int) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onNoteChange: (String) -> Unit,
|
||||
onHideEmailChecked: (Boolean) -> Unit,
|
||||
onDeactivateSendChecked: (Boolean) -> Unit,
|
||||
) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClickLabel = if (isExpanded) {
|
||||
stringResource(id = R.string.options_expanded)
|
||||
} else {
|
||||
stringResource(id = R.string.options_collapsed)
|
||||
},
|
||||
onClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
.padding(16.dp)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.options),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
Icon(
|
||||
painter = if (isExpanded) {
|
||||
painterResource(R.drawable.ic_expand_up)
|
||||
} else {
|
||||
painterResource(R.drawable.ic_expand_down)
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
// Hide all content if not expanded:
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = fadeIn() + slideInVertically(),
|
||||
exit = fadeOut() + slideOutVertically(),
|
||||
modifier = Modifier.clipToBounds(),
|
||||
) {
|
||||
Column {
|
||||
SendDeletionDateChooser(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SendExpirationDateChooser(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenStepper(
|
||||
label = stringResource(id = R.string.maximum_access_count),
|
||||
value = state.common.maxAccessCount,
|
||||
onValueChange = onMaxAccessCountChange,
|
||||
isDecrementEnabled = state.common.maxAccessCount != null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.maximum_access_count_info),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.new_password),
|
||||
hint = stringResource(id = R.string.password_info),
|
||||
value = state.common.passwordInput,
|
||||
onValueChange = onPasswordChange,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.notes),
|
||||
hint = stringResource(id = R.string.notes_info),
|
||||
value = state.common.noteInput,
|
||||
onValueChange = onNoteChange,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.hide_email),
|
||||
isChecked = state.common.isHideEmailChecked,
|
||||
onCheckedChange = onHideEmailChecked,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.disable_send),
|
||||
isChecked = state.common.isDeactivateChecked,
|
||||
onCheckedChange = onDeactivateSendChecked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,30 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.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
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.draw.clipToBounds
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
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.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
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.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.SendDeletionDateChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.SendExpirationDateChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
|
||||
|
||||
/**
|
||||
* Displays new send UX.
|
||||
@@ -104,249 +73,25 @@ fun AddSendScreen(
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(paddingValues = innerPadding),
|
||||
) {
|
||||
BitwardenTextField(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.name),
|
||||
hint = stringResource(id = R.string.name_info),
|
||||
value = state.name,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.NameChange(it)) }
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.type),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenSegmentedButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
options = listOf(
|
||||
SegmentedButtonState(
|
||||
text = stringResource(id = R.string.file),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.FileTypeClick) }
|
||||
},
|
||||
isChecked = state.selectedType is AddSendState.SendType.File,
|
||||
),
|
||||
SegmentedButtonState(
|
||||
text = stringResource(id = R.string.text),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.TextTypeClick) }
|
||||
},
|
||||
isChecked = state.selectedType is AddSendState.SendType.Text,
|
||||
),
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
when (val type = state.selectedType) {
|
||||
is AddSendState.SendType.File -> {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.file),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = R.string.no_file_chosen),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(id = R.string.choose_file),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.ChooseFileClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.max_file_size),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.type_file_info),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
val modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues = innerPadding)
|
||||
|
||||
is AddSendState.SendType.Text -> {
|
||||
BitwardenTextField(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.text),
|
||||
hint = stringResource(id = R.string.type_text_info),
|
||||
value = type.input,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.TextChange(it)) }
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.hide_text_by_default),
|
||||
isChecked = type.isHideByDefaultChecked,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
NewSendOptions(
|
||||
state = state,
|
||||
onMaxAccessCountChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it)) }
|
||||
},
|
||||
onPasswordChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.PasswordChange(it)) }
|
||||
},
|
||||
onNoteChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.NoteChange(it)) }
|
||||
},
|
||||
onHideEmailChecked = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it)) }
|
||||
},
|
||||
onDeactivateSendChecked = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it)) }
|
||||
},
|
||||
when (val viewState = state.viewState) {
|
||||
is AddSendState.ViewState.Content -> AddSendContent(
|
||||
state = viewState,
|
||||
addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) },
|
||||
modifier = modifier,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a collapsable set of new send options.
|
||||
*
|
||||
* @param state state.
|
||||
* @param onMaxAccessCountChange called when max access count changes.
|
||||
* @param onPasswordChange called when the password changes.
|
||||
* @param onNoteChange called when the notes changes.
|
||||
* @param onHideEmailChecked called when hide email is checked.
|
||||
* @param onDeactivateSendChecked called when deactivate send is checked.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewSendOptions(
|
||||
state: AddSendState,
|
||||
onMaxAccessCountChange: (Int) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onNoteChange: (String) -> Unit,
|
||||
onHideEmailChecked: (Boolean) -> Unit,
|
||||
onDeactivateSendChecked: (Boolean) -> Unit,
|
||||
) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClickLabel = if (isExpanded) {
|
||||
stringResource(id = R.string.options_expanded)
|
||||
} else {
|
||||
stringResource(id = R.string.options_collapsed)
|
||||
},
|
||||
onClick = { isExpanded = !isExpanded },
|
||||
is AddSendState.ViewState.Error -> BitwardenErrorContent(
|
||||
message = viewState.message(),
|
||||
modifier = modifier,
|
||||
)
|
||||
.padding(16.dp)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.options),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
Icon(
|
||||
painter = if (isExpanded) {
|
||||
painterResource(R.drawable.ic_expand_up)
|
||||
} else {
|
||||
painterResource(R.drawable.ic_expand_down)
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
// Hide all content if not expanded:
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = fadeIn() + slideInVertically(),
|
||||
exit = fadeOut() + slideOutVertically(),
|
||||
modifier = Modifier.clipToBounds(),
|
||||
) {
|
||||
Column {
|
||||
SendDeletionDateChooser(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
SendExpirationDateChooser(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenStepper(
|
||||
label = stringResource(id = R.string.maximum_access_count),
|
||||
value = state.maxAccessCount,
|
||||
onValueChange = onMaxAccessCountChange,
|
||||
isDecrementEnabled = state.maxAccessCount != null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.maximum_access_count_info),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.new_password),
|
||||
hint = stringResource(id = R.string.password_info),
|
||||
value = state.passwordInput,
|
||||
onValueChange = onPasswordChange,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.notes),
|
||||
hint = stringResource(id = R.string.notes_info),
|
||||
value = state.noteInput,
|
||||
onValueChange = onNoteChange,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.hide_email),
|
||||
isChecked = state.isHideEmailChecked,
|
||||
onCheckedChange = onHideEmailChecked,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = R.string.disable_send),
|
||||
isChecked = state.isDeactivateChecked,
|
||||
onCheckedChange = onDeactivateSendChecked,
|
||||
|
||||
AddSendState.ViewState.Loading -> BitwardenLoadingContent(
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -22,15 +23,19 @@ class AddSendViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: AddSendState(
|
||||
name = "",
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
noteInput = "",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
selectedType = AddSendState.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
viewState = AddSendState.ViewState.Content(
|
||||
common = AddSendState.ViewState.Content.Common(
|
||||
name = "",
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
noteInput = "",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
),
|
||||
selectedType = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
@@ -58,25 +63,25 @@ class AddSendViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handlePasswordChange(action: AddSendAction.PasswordChange) {
|
||||
mutableStateFlow.update {
|
||||
updateCommonContent {
|
||||
it.copy(passwordInput = action.input)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNoteChange(action: AddSendAction.NoteChange) {
|
||||
mutableStateFlow.update {
|
||||
updateCommonContent {
|
||||
it.copy(noteInput = action.input)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHideMyEmailToggle(action: AddSendAction.HideMyEmailToggle) {
|
||||
mutableStateFlow.update {
|
||||
updateCommonContent {
|
||||
it.copy(isHideEmailChecked = action.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeactivateThisSendToggle(action: AddSendAction.DeactivateThisSendToggle) {
|
||||
mutableStateFlow.update {
|
||||
updateCommonContent {
|
||||
it.copy(isDeactivateChecked = action.isChecked)
|
||||
}
|
||||
}
|
||||
@@ -86,36 +91,37 @@ class AddSendViewModel @Inject constructor(
|
||||
private fun handleSaveClick() = sendEvent(AddSendEvent.ShowToast("Save Not Implemented"))
|
||||
|
||||
private fun handleNameChange(action: AddSendAction.NameChange) {
|
||||
mutableStateFlow.update {
|
||||
updateCommonContent {
|
||||
it.copy(name = action.input)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFileTypeClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(selectedType = AddSendState.SendType.File)
|
||||
updateContent {
|
||||
it.copy(selectedType = AddSendState.ViewState.Content.SendType.File)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTextTypeClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(selectedType = AddSendState.SendType.Text("", isHideByDefaultChecked = false))
|
||||
updateContent {
|
||||
it.copy(
|
||||
selectedType = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTextChange(action: AddSendAction.TextChange) {
|
||||
val currentSendInput =
|
||||
mutableStateFlow.value.selectedType as? AddSendState.SendType.Text ?: return
|
||||
mutableStateFlow.update {
|
||||
it.copy(selectedType = currentSendInput.copy(input = action.input))
|
||||
updateTextContent {
|
||||
it.copy(input = action.input)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHideByDefaultToggle(action: AddSendAction.HideByDefaultToggle) {
|
||||
val currentSendInput =
|
||||
mutableStateFlow.value.selectedType as? AddSendState.SendType.Text ?: return
|
||||
mutableStateFlow.update {
|
||||
it.copy(selectedType = currentSendInput.copy(isHideByDefaultChecked = action.isChecked))
|
||||
updateTextContent {
|
||||
it.copy(isHideByDefaultChecked = action.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +131,54 @@ class AddSendViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleMaxAccessCountChange(action: AddSendAction.MaxAccessCountChange) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(maxAccessCount = action.value)
|
||||
updateCommonContent { it.copy(maxAccessCount = action.value) }
|
||||
}
|
||||
|
||||
private inline fun onContent(
|
||||
crossinline block: (AddSendState.ViewState.Content) -> Unit,
|
||||
) {
|
||||
(state.viewState as? AddSendState.ViewState.Content)?.let(block)
|
||||
}
|
||||
|
||||
private inline fun updateContent(
|
||||
crossinline block: (
|
||||
AddSendState.ViewState.Content,
|
||||
) -> AddSendState.ViewState.Content?,
|
||||
) {
|
||||
val currentViewState = state.viewState
|
||||
val updatedContent = (currentViewState as? AddSendState.ViewState.Content)
|
||||
?.let(block)
|
||||
?: return
|
||||
mutableStateFlow.update { it.copy(viewState = updatedContent) }
|
||||
}
|
||||
|
||||
private inline fun updateCommonContent(
|
||||
crossinline block: (
|
||||
AddSendState.ViewState.Content.Common,
|
||||
) -> AddSendState.ViewState.Content.Common,
|
||||
) {
|
||||
updateContent { it.copy(common = block(it.common)) }
|
||||
}
|
||||
|
||||
private inline fun updateFileContent(
|
||||
crossinline block: (
|
||||
AddSendState.ViewState.Content.SendType.File,
|
||||
) -> AddSendState.ViewState.Content.SendType.File,
|
||||
) {
|
||||
updateContent { currentContent ->
|
||||
(currentContent.selectedType as? AddSendState.ViewState.Content.SendType.File)
|
||||
?.let { currentContent.copy(selectedType = block(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun updateTextContent(
|
||||
crossinline block: (
|
||||
AddSendState.ViewState.Content.SendType.Text,
|
||||
) -> AddSendState.ViewState.Content.SendType.Text,
|
||||
) {
|
||||
updateContent { currentContent ->
|
||||
(currentContent.selectedType as? AddSendState.ViewState.Content.SendType.Text)
|
||||
?.let { currentContent.copy(selectedType = block(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,34 +188,68 @@ class AddSendViewModel @Inject constructor(
|
||||
*/
|
||||
@Parcelize
|
||||
data class AddSendState(
|
||||
val name: String,
|
||||
val selectedType: SendType,
|
||||
// Null here means "not set"
|
||||
val maxAccessCount: Int?,
|
||||
val passwordInput: String,
|
||||
val noteInput: String,
|
||||
val isHideEmailChecked: Boolean,
|
||||
val isDeactivateChecked: Boolean,
|
||||
val viewState: ViewState,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Models what type the user is trying to send.
|
||||
* Represents the specific view states for the [AddSendScreen].
|
||||
*/
|
||||
sealed class SendType : Parcelable {
|
||||
sealed class ViewState : Parcelable {
|
||||
/**
|
||||
* Sending a file.
|
||||
* Represents an error state for the [AddSendScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data object File : SendType()
|
||||
data class Error(val message: Text) : ViewState()
|
||||
|
||||
/**
|
||||
* Sending text.
|
||||
* Loading state for the [AddSendScreen], signifying that the content is being processed.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Text(
|
||||
val input: String,
|
||||
val isHideByDefaultChecked: Boolean,
|
||||
) : SendType()
|
||||
data object Loading : ViewState()
|
||||
|
||||
/**
|
||||
* Represents a loaded content state for the [AddSendScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Content(
|
||||
val common: Common,
|
||||
val selectedType: SendType,
|
||||
) : ViewState() {
|
||||
|
||||
/**
|
||||
* Content data that is common for all item types.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Common(
|
||||
val name: String,
|
||||
// Null here means "not set"
|
||||
val maxAccessCount: Int?,
|
||||
val passwordInput: String,
|
||||
val noteInput: String,
|
||||
val isHideEmailChecked: Boolean,
|
||||
val isDeactivateChecked: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Models what type the user is trying to send.
|
||||
*/
|
||||
sealed class SendType : Parcelable {
|
||||
/**
|
||||
* Sending a file.
|
||||
*/
|
||||
@Parcelize
|
||||
data object File : SendType()
|
||||
|
||||
/**
|
||||
* Sending text.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Text(
|
||||
val input: String,
|
||||
val isHideByDefaultChecked: Boolean,
|
||||
) : SendType()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendAction
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of adding
|
||||
* send items.
|
||||
*/
|
||||
data class AddSendHandlers(
|
||||
val onNamChange: (String) -> Unit,
|
||||
val onFileTypeSelect: () -> Unit,
|
||||
val onTextTypeSelect: () -> Unit,
|
||||
val onChooseFileCLick: () -> Unit,
|
||||
val onTextChange: (String) -> Unit,
|
||||
val onIsHideByDefaultToggle: (Boolean) -> Unit,
|
||||
val onMaxAccessCountChange: (Int) -> Unit,
|
||||
val onPasswordChange: (String) -> Unit,
|
||||
val onNoteChange: (String) -> Unit,
|
||||
val onHideEmailToggle: (Boolean) -> Unit,
|
||||
val onDeactivateSendToggle: (Boolean) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates an instance of [AddSendHandlers] by binding actions to the provided
|
||||
* [AddSendViewModel].
|
||||
*/
|
||||
fun create(
|
||||
viewModel: AddSendViewModel,
|
||||
): AddSendHandlers =
|
||||
AddSendHandlers(
|
||||
onNamChange = { viewModel.trySendAction(AddSendAction.NameChange(it)) },
|
||||
onFileTypeSelect = { viewModel.trySendAction(AddSendAction.FileTypeClick) },
|
||||
onTextTypeSelect = { viewModel.trySendAction(AddSendAction.TextTypeClick) },
|
||||
onChooseFileCLick = { viewModel.trySendAction(AddSendAction.ChooseFileClick) },
|
||||
onTextChange = { viewModel.trySendAction(AddSendAction.TextChange(it)) },
|
||||
onIsHideByDefaultToggle = {
|
||||
viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it))
|
||||
},
|
||||
onMaxAccessCountChange = {
|
||||
viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it))
|
||||
},
|
||||
onPasswordChange = { viewModel.trySendAction(AddSendAction.PasswordChange(it)) },
|
||||
onNoteChange = { viewModel.trySendAction(AddSendAction.NoteChange(it)) },
|
||||
onHideEmailToggle = {
|
||||
viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it))
|
||||
},
|
||||
onDeactivateSendToggle = {
|
||||
viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user