mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
Update Authenticator UI to match Password Manager style (#5969)
This commit is contained in:
parent
ca474b272a
commit
2636a4f93a
@ -9,17 +9,19 @@ import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.bitwarden.authenticator.ui.platform.composition.LocalManagerProvider
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.setupEdgeToEdge
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -39,6 +41,9 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
var shouldShowSplashScreen = true
|
||||
@ -53,13 +58,14 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
|
||||
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
observeViewModelEvents(navController)
|
||||
LocalManagerProvider {
|
||||
AuthenticatorTheme(
|
||||
BitwardenTheme(
|
||||
theme = state.theme,
|
||||
) {
|
||||
RootNavScreen(
|
||||
@ -94,6 +100,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.UpdateAppTheme -> {
|
||||
AppCompatDelegate.setDefaultNightMode(event.osTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
@ -60,6 +60,7 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
private fun handleThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
|
||||
mutableStateFlow.update { it.copy(theme = action.theme) }
|
||||
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
|
||||
}
|
||||
|
||||
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
|
||||
@ -139,4 +140,9 @@ sealed class MainEvent {
|
||||
* Event indicating a change in the screen capture setting.
|
||||
*/
|
||||
data class ScreenCaptureSettingChange(val isAllowed: Boolean) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app theme has been updated.
|
||||
*/
|
||||
data class UpdateAppTheme(val osTheme: Int) : MainEvent()
|
||||
}
|
||||
|
||||
@ -2,16 +2,15 @@ package com.bitwarden.authenticator.ui.auth.unlock
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -25,18 +24,16 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.composition.LocalBiometricsManager
|
||||
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Top level composable for the unlock screen.
|
||||
@ -60,10 +57,8 @@ fun UnlockScreen(
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is UnlockState.Dialog.Error -> BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = dialog.message,
|
||||
),
|
||||
title = stringResource(id = BitwardenString.an_error_has_occurred),
|
||||
message = dialog.message(),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(UnlockAction.DismissDialog)
|
||||
@ -72,7 +67,7 @@ fun UnlockScreen(
|
||||
)
|
||||
|
||||
UnlockState.Dialog.Loading -> BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(BitwardenString.loading.asText()),
|
||||
text = stringResource(id = BitwardenString.loading),
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
@ -107,45 +102,44 @@ fun UnlockScreen(
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) { innerPadding ->
|
||||
Box {
|
||||
Column(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.width(220.dp)
|
||||
.height(74.dp)
|
||||
.fillMaxWidth(),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_logo_horizontal),
|
||||
contentDescription = stringResource(BitwardenString.bitwarden_authenticator),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
AuthenticatorFilledTonalButton(
|
||||
label = stringResource(id = BitwardenString.use_biometrics_to_unlock),
|
||||
onClick = {
|
||||
biometricsManager.promptBiometrics(
|
||||
onSuccess = onBiometricsUnlock,
|
||||
onCancel = {
|
||||
// no-op
|
||||
},
|
||||
onError = {
|
||||
// no-op
|
||||
},
|
||||
onLockOut = onBiometricsLockOut,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp)
|
||||
.width(220.dp)
|
||||
.height(74.dp)
|
||||
.fillMaxWidth(),
|
||||
colorFilter = ColorFilter.tint(BitwardenTheme.colorScheme.icon.secondary),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_logo_horizontal),
|
||||
contentDescription = stringResource(BitwardenString.bitwarden_authenticator),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.use_biometrics_to_unlock),
|
||||
onClick = {
|
||||
biometricsManager.promptBiometrics(
|
||||
onSuccess = onBiometricsUnlock,
|
||||
onCancel = {
|
||||
// no-op
|
||||
},
|
||||
onError = {
|
||||
// no-op
|
||||
},
|
||||
onLockOut = onBiometricsLockOut,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,44 +1,31 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.edititem
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
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.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.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
@ -46,24 +33,23 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.model.EditItemData
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorErrorContent
|
||||
import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorLoadingContent
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.components.stepper.BitwardenStepper
|
||||
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenExpandingHeader
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.stepper.BitwardenStepper
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
@ -82,7 +68,7 @@ fun EditItemScreen(
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val resources = LocalResources.current
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
@ -112,10 +98,8 @@ fun EditItemScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
title = stringResource(
|
||||
id = BitwardenString.edit,
|
||||
),
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.edit),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
@ -125,7 +109,7 @@ fun EditItemScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
AuthenticatorTextButton(
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.save),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EditItemAction.SaveClick) }
|
||||
@ -136,12 +120,10 @@ fun EditItemScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.EndOverlay,
|
||||
) { innerPadding ->
|
||||
) {
|
||||
when (val viewState = state.viewState) {
|
||||
is EditItemState.ViewState.Content -> {
|
||||
EditItemContent(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding),
|
||||
viewState = viewState,
|
||||
onIssuerNameTextChange = remember(viewModel) {
|
||||
{
|
||||
@ -210,16 +192,11 @@ fun EditItemScreen(
|
||||
}
|
||||
|
||||
is EditItemState.ViewState.Error -> {
|
||||
AuthenticatorErrorContent(
|
||||
message = viewState.message(),
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
BitwardenErrorContent(message = viewState.message())
|
||||
}
|
||||
|
||||
EditItemState.ViewState.Loading -> {
|
||||
AuthenticatorLoadingContent(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
BitwardenLoadingContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,9 +222,11 @@ fun EditItemContent(
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
label = stringResource(id = BitwardenString.information),
|
||||
)
|
||||
@ -264,11 +243,11 @@ fun EditItemContent(
|
||||
value = viewState.itemData.issuer,
|
||||
onValueChange = onIssuerNameTextChange,
|
||||
singleLine = true,
|
||||
cardStyle = CardStyle.Top(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordField(
|
||||
modifier = Modifier
|
||||
.testTag(tag = "KeyTextField")
|
||||
@ -278,12 +257,11 @@ fun EditItemContent(
|
||||
value = viewState.itemData.totpCode,
|
||||
onValueChange = onTotpCodeTextChange,
|
||||
singleLine = true,
|
||||
capitalization = KeyboardCapitalization.Characters,
|
||||
cardStyle = CardStyle.Middle(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
modifier = Modifier
|
||||
.testTag(tag = "UsernameTextField")
|
||||
@ -293,15 +271,16 @@ fun EditItemContent(
|
||||
value = viewState.itemData.username.orEmpty(),
|
||||
onValueChange = onUsernameTextChange,
|
||||
singleLine = true,
|
||||
cardStyle = CardStyle.Middle(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = BitwardenString.favorite),
|
||||
isChecked = viewState.itemData.favorite,
|
||||
onCheckedChange = onToggleFavorite,
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "ItemFavoriteToggle")
|
||||
.fillMaxWidth()
|
||||
@ -310,47 +289,13 @@ fun EditItemContent(
|
||||
}
|
||||
|
||||
item(key = "AdvancedOptions") {
|
||||
val iconRotationDegrees = animateFloatAsState(
|
||||
targetValue = if (viewState.isAdvancedOptionsExpanded) 180f else 0f,
|
||||
label = "expanderIconRotationAnimation",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(
|
||||
BitwardenExpandingHeader(
|
||||
isExpanded = viewState.isAdvancedOptionsExpanded,
|
||||
onClick = onExpandAdvancedOptionsClicked,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "CollapseAdvancedOptions")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(28.dp))
|
||||
.clickable(
|
||||
indication = ripple(
|
||||
bounded = true,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onExpandAdvancedOptionsClicked,
|
||||
)
|
||||
.padding(vertical = 12.dp)
|
||||
.animateItem(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(BitwardenString.advanced),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_down),
|
||||
contentDescription = if (viewState.isAdvancedOptionsExpanded) {
|
||||
stringResource(BitwardenString.collapse_advanced_options)
|
||||
} else {
|
||||
stringResource(BitwardenString.expand_advanced_options)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.rotate(degrees = iconRotationDegrees.value),
|
||||
)
|
||||
}
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
if (viewState.isAdvancedOptionsExpanded) {
|
||||
@ -380,8 +325,7 @@ private fun LazyListScope.advancedOptions(
|
||||
) {
|
||||
item(key = "OtpItemTypeSelector") {
|
||||
val possibleTypeOptions = AuthenticatorItemType.entries
|
||||
val typeOptionsWithStrings =
|
||||
possibleTypeOptions.associateWith { it.name }
|
||||
val typeOptionsWithStrings = possibleTypeOptions.associateWith { it.name }
|
||||
BitwardenMultiSelectButton(
|
||||
modifier = Modifier
|
||||
.testTag(tag = "OTPItemTypePicker")
|
||||
@ -391,6 +335,7 @@ private fun LazyListScope.advancedOptions(
|
||||
label = stringResource(id = BitwardenString.otp_type),
|
||||
options = typeOptionsWithStrings.values.toImmutableList(),
|
||||
selectedOption = viewState.itemData.type.name,
|
||||
cardStyle = CardStyle.Top(),
|
||||
onOptionSelected = { selectedOption ->
|
||||
val selectedOptionName = typeOptionsWithStrings
|
||||
.entries
|
||||
@ -404,7 +349,6 @@ private fun LazyListScope.advancedOptions(
|
||||
item(key = "AlgorithmItemTypeSelector") {
|
||||
val possibleAlgorithmOptions = AuthenticatorItemAlgorithm.entries
|
||||
val algorithmOptionsWithStrings = possibleAlgorithmOptions.associateWith { it.name }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
BitwardenMultiSelectButton(
|
||||
modifier = Modifier
|
||||
.testTag(tag = "AlgorithmItemTypePicker")
|
||||
@ -414,6 +358,7 @@ private fun LazyListScope.advancedOptions(
|
||||
label = stringResource(id = BitwardenString.algorithm),
|
||||
options = algorithmOptionsWithStrings.values.toImmutableList(),
|
||||
selectedOption = viewState.itemData.algorithm.name,
|
||||
cardStyle = CardStyle.Middle(),
|
||||
onOptionSelected = { selectedOption ->
|
||||
val selectedOptionName = algorithmOptionsWithStrings
|
||||
.entries
|
||||
@ -433,7 +378,6 @@ private fun LazyListScope.advancedOptions(
|
||||
formatArgs = arrayOf(it.seconds),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenMultiSelectButton(
|
||||
modifier = Modifier
|
||||
.testTag(tag = "RefreshPeriodItemTypePicker")
|
||||
@ -445,6 +389,7 @@ private fun LazyListScope.advancedOptions(
|
||||
selectedOption = refreshPeriodOptionsWithStrings.getValue(
|
||||
key = viewState.itemData.refreshPeriod,
|
||||
),
|
||||
cardStyle = CardStyle.Middle(),
|
||||
onOptionSelected = remember(viewState) {
|
||||
{ selectedOption ->
|
||||
val selectedOptionName = refreshPeriodOptionsWithStrings
|
||||
@ -458,7 +403,6 @@ private fun LazyListScope.advancedOptions(
|
||||
}
|
||||
|
||||
item(key = "DigitsCounterItem") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
DigitsCounterItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -480,17 +424,15 @@ private fun EditItemDialogs(
|
||||
when (dialogState) {
|
||||
is EditItemState.DialogState.Generic -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialogState.title,
|
||||
message = dialogState.message,
|
||||
),
|
||||
title = dialogState.title(),
|
||||
message = dialogState.message(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is EditItemState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(dialogState.message),
|
||||
text = dialogState.message(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -511,8 +453,7 @@ private fun DigitsCounterItem(
|
||||
value = digits.coerceIn(minValue, maxValue),
|
||||
range = minValue..maxValue,
|
||||
onValueChange = onDigitsCounterChange,
|
||||
increaseButtonTestTag = "DigitsIncreaseButton",
|
||||
decreaseButtonTestTag = "DigitsDecreaseButton",
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = modifier.testTag(tag = "DigitsValueLabel"),
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
@ -21,6 +19,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Show a snackbar that says "Account synced from Bitwarden app" with a close action.
|
||||
@ -40,8 +39,8 @@ fun FirstTimeSyncSnackbarHost(
|
||||
.fillMaxWidth()
|
||||
.shadow(elevation = 6.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.inverseSurface,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = BitwardenTheme.colorScheme.background.alert,
|
||||
shape = BitwardenTheme.shapes.snackbar,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@ -50,8 +49,8 @@ fun FirstTimeSyncSnackbarHost(
|
||||
.padding(16.dp)
|
||||
.weight(1f, fill = true),
|
||||
text = stringResource(BitwardenString.account_synced_from_bitwarden_app),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.reversed,
|
||||
)
|
||||
IconButton(
|
||||
onClick = { state.currentSnackbarData?.dismiss() },
|
||||
@ -59,7 +58,7 @@ fun FirstTimeSyncSnackbarHost(
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_close),
|
||||
contentDescription = stringResource(id = BitwardenString.close),
|
||||
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.reversed,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@ package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
||||
|
||||
import android.Manifest
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -10,22 +11,19 @@ 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.layout.size
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@ -43,10 +41,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -57,30 +54,29 @@ import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.It
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.model.VerificationCodeDisplayItem
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorMediumTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.action.AuthenticatorSearchActionItem
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.card.AuthenticatorActionCard
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon
|
||||
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.AuthenticatorExpandingHeader
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderTextWithSupportLabel
|
||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.composition.LocalPermissionsManager
|
||||
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.bitwarden.authenticator.ui.platform.theme.Typography
|
||||
import com.bitwarden.authenticator.ui.platform.util.startAuthenticatorAppSettings
|
||||
import com.bitwarden.authenticator.ui.platform.util.startBitwardenAccountSettings
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.action.BitwardenSearchActionItem
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||
import com.bitwarden.ui.platform.components.card.color.bitwardenCardColors
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.fab.BitwardenExpandableFloatingActionButton
|
||||
import com.bitwarden.ui.platform.components.fab.ExpandableFabIcon
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
@ -89,6 +85,7 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@ -310,18 +307,14 @@ private fun ItemListingDialogs(
|
||||
when (dialog) {
|
||||
ItemListingState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = BitwardenString.syncing.asText(),
|
||||
),
|
||||
text = stringResource(id = BitwardenString.syncing),
|
||||
)
|
||||
}
|
||||
|
||||
is ItemListingState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialog.title,
|
||||
message = dialog.message,
|
||||
),
|
||||
title = dialog.title(),
|
||||
message = dialog.message(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
@ -368,11 +361,11 @@ private fun ItemListingContent(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorMediumTopAppBar(
|
||||
BitwardenMediumTopAppBar(
|
||||
title = stringResource(id = BitwardenString.verification_codes),
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
AuthenticatorSearchActionItem(
|
||||
BitwardenSearchActionItem(
|
||||
contentDescription = stringResource(id = BitwardenString.search_codes),
|
||||
onClick = onNavigateToSearch,
|
||||
)
|
||||
@ -380,53 +373,43 @@ private fun ItemListingContent(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExpandableFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "AddItemButton" }
|
||||
.padding(bottom = 16.dp),
|
||||
label = BitwardenString.add_item.asText(),
|
||||
items = listOf(
|
||||
BitwardenExpandableFloatingActionButton(
|
||||
modifier = Modifier.testTag("AddItemButton"),
|
||||
items = persistentListOf(
|
||||
ItemListingExpandableFabAction.ScanQrCode(
|
||||
label = BitwardenString.scan_a_qr_code.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_camera),
|
||||
contentDescription = stringResource(
|
||||
id = BitwardenString.scan_a_qr_code,
|
||||
),
|
||||
icon = IconData.Local(
|
||||
iconRes = BitwardenDrawable.ic_camera_small,
|
||||
contentDescription = BitwardenString.scan_a_qr_code.asText(),
|
||||
testTag = "ScanQRCodeButton",
|
||||
),
|
||||
onScanQrCodeClick = onScanQrCodeClick,
|
||||
),
|
||||
ItemListingExpandableFabAction.EnterSetupKey(
|
||||
label = BitwardenString.enter_key_manually.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_keyboard),
|
||||
contentDescription = stringResource(
|
||||
id = BitwardenString.enter_key_manually,
|
||||
),
|
||||
icon = IconData.Local(
|
||||
iconRes = BitwardenDrawable.ic_lock_encrypted_small,
|
||||
contentDescription = BitwardenString.enter_key_manually.asText(),
|
||||
testTag = "EnterSetupKeyButton",
|
||||
),
|
||||
onEnterSetupKeyClick = onEnterSetupKeyClick,
|
||||
),
|
||||
),
|
||||
expandableFabIcon = ExpandableFabIcon(
|
||||
iconData = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_plus),
|
||||
contentDescription = stringResource(id = BitwardenString.add_item),
|
||||
icon = IconData.Local(
|
||||
iconRes = BitwardenDrawable.ic_plus,
|
||||
contentDescription = BitwardenString.add_item.asText(),
|
||||
testTag = "AddItemButton",
|
||||
),
|
||||
iconRotation = 45f,
|
||||
),
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.EndOverlay,
|
||||
snackbarHost = { FirstTimeSyncSnackbarHost(state = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
) {
|
||||
var isLocalHeaderExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
item(key = "action_card") {
|
||||
ActionCard(
|
||||
@ -437,30 +420,29 @@ private fun ItemListingContent(
|
||||
onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick,
|
||||
onSyncLearnMoreClick = onSyncLearnMoreClick,
|
||||
modifier = Modifier
|
||||
.padding(all = 16.dp)
|
||||
.standardHorizontalMargin()
|
||||
.padding(top = 12.dp, bottom = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
if (state.favoriteItems.isNotEmpty()) {
|
||||
item(key = "favorites_header") {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.favorites),
|
||||
supportingLabel = state.favoriteItems.count().toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = state.favoriteItems,
|
||||
key = { "favorite_item_${it.id}" },
|
||||
) {
|
||||
key = { _, it -> "favorite_item_${it.id}" },
|
||||
) { index, it ->
|
||||
VaultVerificationCodeItem(
|
||||
authCode = it.authCode,
|
||||
primaryLabel = it.title,
|
||||
@ -475,18 +457,10 @@ private fun ItemListingContent(
|
||||
},
|
||||
showMoveToBitwarden = it.showMoveToBitwarden,
|
||||
allowLongPress = it.allowLongPressActions,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "favorites_divider") {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
cardStyle = state.favoriteItems.toListItemCardStyle(index = index),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp)
|
||||
.animateItem(),
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -511,16 +485,17 @@ private fun ItemListingContent(
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLocalHeaderExpanded) {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = state.itemList,
|
||||
key = { "local_item_${it.id}" },
|
||||
) {
|
||||
key = { _, it -> "local_item_${it.id}" },
|
||||
) { index, it ->
|
||||
VaultVerificationCodeItem(
|
||||
authCode = it.authCode,
|
||||
primaryLabel = it.title,
|
||||
@ -535,7 +510,9 @@ private fun ItemListingContent(
|
||||
},
|
||||
showMoveToBitwarden = it.showMoveToBitwarden,
|
||||
allowLongPress = it.allowLongPressActions,
|
||||
cardStyle = state.itemList.toListItemCardStyle(index = index),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
)
|
||||
@ -561,14 +538,15 @@ private fun ItemListingContent(
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
if (section.isExpanded) {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = section.codes,
|
||||
key = { code -> "code_${code.id}" },
|
||||
) {
|
||||
key = { _, code -> "code_${code.id}" },
|
||||
) { index, it ->
|
||||
VaultVerificationCodeItem(
|
||||
authCode = it.authCode,
|
||||
primaryLabel = it.title,
|
||||
@ -583,7 +561,9 @@ private fun ItemListingContent(
|
||||
},
|
||||
showMoveToBitwarden = it.showMoveToBitwarden,
|
||||
allowLongPress = it.allowLongPressActions,
|
||||
cardStyle = section.codes.toListItemCardStyle(index = index),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
)
|
||||
@ -596,9 +576,10 @@ private fun ItemListingContent(
|
||||
item(key = "shared_codes_error") {
|
||||
Text(
|
||||
text = stringResource(BitwardenString.shared_codes_error),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
@ -609,7 +590,8 @@ private fun ItemListingContent(
|
||||
// Add a spacer item to prevent the FAB from hiding verification codes at the
|
||||
// bottom of the list
|
||||
item {
|
||||
Spacer(Modifier.height(72.dp))
|
||||
Spacer(modifier = Modifier.height(height = 88.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -642,59 +624,49 @@ fun EmptyItemListingContent(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.verification_codes),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
actions = { },
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExpandableFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "AddItemButton" }
|
||||
.padding(bottom = 16.dp),
|
||||
label = BitwardenString.add_item.asText(),
|
||||
items = listOf(
|
||||
BitwardenExpandableFloatingActionButton(
|
||||
modifier = Modifier.testTag("AddItemButton"),
|
||||
items = persistentListOf(
|
||||
ItemListingExpandableFabAction.ScanQrCode(
|
||||
label = BitwardenString.scan_a_qr_code.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_camera),
|
||||
contentDescription = stringResource(
|
||||
id = BitwardenString.scan_a_qr_code,
|
||||
),
|
||||
icon = IconData.Local(
|
||||
iconRes = BitwardenDrawable.ic_camera_small,
|
||||
contentDescription = BitwardenString.scan_a_qr_code.asText(),
|
||||
testTag = "ScanQRCodeButton",
|
||||
),
|
||||
onScanQrCodeClick = onScanQrCodeClick,
|
||||
),
|
||||
ItemListingExpandableFabAction.EnterSetupKey(
|
||||
label = BitwardenString.enter_key_manually.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_keyboard),
|
||||
contentDescription = stringResource(
|
||||
id = BitwardenString.enter_key_manually,
|
||||
),
|
||||
icon = IconData.Local(
|
||||
iconRes = BitwardenDrawable.ic_lock_encrypted_small,
|
||||
contentDescription = BitwardenString.enter_key_manually.asText(),
|
||||
testTag = "EnterSetupKeyButton",
|
||||
),
|
||||
onEnterSetupKeyClick = onEnterSetupKeyClick,
|
||||
),
|
||||
),
|
||||
expandableFabIcon = ExpandableFabIcon(
|
||||
iconData = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_plus),
|
||||
contentDescription = stringResource(id = BitwardenString.add_item),
|
||||
icon = IconData.Local(
|
||||
iconRes = BitwardenDrawable.ic_plus,
|
||||
contentDescription = BitwardenString.add_item.asText(),
|
||||
testTag = "AddItemButton",
|
||||
),
|
||||
iconRotation = 45f,
|
||||
),
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.EndOverlay,
|
||||
) { innerPadding ->
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues = innerPadding)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = when (actionCardState) {
|
||||
ItemListingState.ActionCardState.None -> Arrangement.Center
|
||||
@ -709,19 +681,15 @@ fun EmptyItemListingContent(
|
||||
onSyncWithBitwardenClick = onSyncWithBitwardenClick,
|
||||
onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick,
|
||||
onSyncLearnMoreClick = onSyncLearnMoreClick,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(top = 12.dp, bottom = 16.dp),
|
||||
)
|
||||
|
||||
// Add a spacer if an action card is showing:
|
||||
when (actionCardState) {
|
||||
ItemListingState.ActionCardState.None -> Unit
|
||||
ItemListingState.ActionCardState.DownloadBitwardenApp,
|
||||
ItemListingState.ActionCardState.SyncWithBitwarden,
|
||||
-> Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
.standardHorizontalMargin(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
@ -744,7 +712,7 @@ fun EmptyItemListingContent(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.you_dont_have_items_to_display),
|
||||
style = Typography.titleMedium,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@ -754,13 +722,16 @@ fun EmptyItemListingContent(
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AuthenticatorFilledTonalButton(
|
||||
BitwardenFilledButton(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "AddCodeButton" }
|
||||
.testTag("AddCodeButton")
|
||||
.fillMaxWidth(),
|
||||
label = stringResource(BitwardenString.add_code),
|
||||
onClick = onAddCodeClick,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -771,26 +742,20 @@ private fun DownloadBitwardenActionCard(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismissClick: () -> Unit,
|
||||
onDownloadBitwardenClick: () -> Unit,
|
||||
) = AuthenticatorActionCard(
|
||||
) = BitwardenActionCard(
|
||||
modifier = modifier,
|
||||
actionIcon = rememberVectorPainter(BitwardenDrawable.ic_shield),
|
||||
actionText = stringResource(BitwardenString.download_bitwarden_card_message),
|
||||
callToActionText = stringResource(BitwardenString.download_now),
|
||||
titleText = stringResource(BitwardenString.download_bitwarden_card_title),
|
||||
onCardClicked = onDownloadBitwardenClick,
|
||||
trailingContent = {
|
||||
IconButton(
|
||||
onClick = onDismissClick,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_close),
|
||||
contentDescription = stringResource(id = BitwardenString.close),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
)
|
||||
}
|
||||
cardSubtitle = stringResource(BitwardenString.download_bitwarden_card_message),
|
||||
actionText = stringResource(BitwardenString.download_now),
|
||||
cardTitle = stringResource(BitwardenString.download_bitwarden_card_title),
|
||||
onActionClick = onDownloadBitwardenClick,
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(BitwardenDrawable.ic_shield),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||
)
|
||||
},
|
||||
onDismissClick = onDismissClick,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@ -803,12 +768,10 @@ private fun SyncWithBitwardenActionCard(
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(size = 16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
shape = BitwardenTheme.shapes.actionCard,
|
||||
colors = bitwardenCardColors(),
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
border = BorderStroke(width = 1.dp, color = BitwardenTheme.colorScheme.stroke.border),
|
||||
) {
|
||||
Spacer(Modifier.height(height = 4.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
@ -820,14 +783,14 @@ private fun SyncWithBitwardenActionCard(
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ic_shield),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||
modifier = Modifier.size(size = 20.dp),
|
||||
)
|
||||
Spacer(Modifier.width(width = 16.dp))
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.sync_with_the_bitwarden_app),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.weight(weight = 1f))
|
||||
@ -836,7 +799,7 @@ private fun SyncWithBitwardenActionCard(
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_close),
|
||||
contentDescription = stringResource(id = BitwardenString.close),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.size(size = 24.dp),
|
||||
)
|
||||
}
|
||||
@ -844,22 +807,22 @@ private fun SyncWithBitwardenActionCard(
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.sync_with_bitwarden_action_card_message),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(start = 36.dp, end = 48.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
AuthenticatorFilledButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.take_me_to_app_settings),
|
||||
onClick = onAppSettingsClick,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
AuthenticatorTextButton(
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.learn_more),
|
||||
onClick = onLearnMoreClick,
|
||||
modifier = Modifier
|
||||
|
||||
@ -6,6 +6,7 @@ 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.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -14,7 +15,6 @@ import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -24,20 +24,22 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
|
||||
import com.bitwarden.authenticator.ui.platform.components.indicator.AuthenticatorCircularCountdownIndicator
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.base.util.cardBackground
|
||||
import com.bitwarden.ui.platform.base.util.cardPadding
|
||||
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The verification code item displayed to the user.
|
||||
@ -69,41 +71,46 @@ fun VaultVerificationCodeItem(
|
||||
onDropdownMenuClick: (VaultDropdownMenuAction) -> Unit,
|
||||
allowLongPress: Boolean,
|
||||
showMoveToBitwarden: Boolean,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
|
||||
Box(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "Item" }
|
||||
.testTag(tag = "Item")
|
||||
.defaultMinSize(minHeight = 60.dp)
|
||||
.cardBackground(cardStyle = cardStyle)
|
||||
.then(
|
||||
if (allowLongPress) {
|
||||
Modifier.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
indication = ripple(
|
||||
color = BitwardenTheme.colorScheme.background.pressed,
|
||||
),
|
||||
onClick = onItemClick,
|
||||
onLongClick = { shouldShowDropdownMenu = true },
|
||||
)
|
||||
} else {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
indication = ripple(
|
||||
color = BitwardenTheme.colorScheme.background.pressed,
|
||||
),
|
||||
onClick = onItemClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
.defaultMinSize(minHeight = 72.dp)
|
||||
.padding(
|
||||
vertical = 8.dp,
|
||||
horizontal = 16.dp,
|
||||
)
|
||||
.then(modifier),
|
||||
.cardPadding(
|
||||
cardStyle = cardStyle,
|
||||
paddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
BitwardenIcon(
|
||||
iconData = startIcon,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
@ -114,10 +121,10 @@ fun VaultVerificationCodeItem(
|
||||
) {
|
||||
if (!primaryLabel.isNullOrEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.semantics { testTag = "Name" },
|
||||
modifier = Modifier.testTag("Name"),
|
||||
text = primaryLabel,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@ -125,34 +132,36 @@ fun VaultVerificationCodeItem(
|
||||
|
||||
if (!secondaryLabel.isNullOrEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.semantics { testTag = "Username" },
|
||||
modifier = Modifier.testTag("Username"),
|
||||
text = secondaryLabel,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticatorCircularCountdownIndicator(
|
||||
modifier = Modifier.semantics { testTag = "CircularCountDown" },
|
||||
BitwardenCircularCountdownIndicator(
|
||||
modifier = Modifier.testTag("CircularCountDown"),
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.semantics { testTag = "AuthCode" },
|
||||
modifier = Modifier.testTag("AuthCode"),
|
||||
text = authCode.chunked(3).joinToString(" "),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.sensitiveInfoSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = shouldShowDropdownMenu,
|
||||
onDismissRequest = { shouldShowDropdownMenu = false },
|
||||
shape = BitwardenTheme.shapes.menu,
|
||||
containerColor = BitwardenTheme.colorScheme.background.primary,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
@ -229,7 +238,7 @@ fun VaultVerificationCodeItem(
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun VerificationCodeItem_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenTheme {
|
||||
VaultVerificationCodeItem(
|
||||
authCode = "1234567890".chunked(3).joinToString(" "),
|
||||
primaryLabel = "Issuer, AKA Name",
|
||||
@ -243,6 +252,7 @@ private fun VerificationCodeItem_preview() {
|
||||
allowLongPress = true,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
showMoveToBitwarden = true,
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
|
||||
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabOption
|
||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.ui.platform.components.fab.ExpandableFabOption
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* Models [ExpandableFabOption]s that can be triggered by the [ExtendedFloatingActionButton].
|
||||
*/
|
||||
sealed class ItemListingExpandableFabAction(
|
||||
label: Text?,
|
||||
icon: IconResource,
|
||||
label: Text,
|
||||
icon: IconData.Local,
|
||||
onFabOptionClick: () -> Unit,
|
||||
) : ExpandableFabOption(label, icon, onFabOptionClick) {
|
||||
|
||||
@ -18,25 +18,25 @@ sealed class ItemListingExpandableFabAction(
|
||||
* Indicates the Scan QR code button was clicked.
|
||||
*/
|
||||
class ScanQrCode(
|
||||
label: Text?,
|
||||
icon: IconResource,
|
||||
label: Text,
|
||||
icon: IconData.Local,
|
||||
onScanQrCodeClick: () -> Unit,
|
||||
) : ItemListingExpandableFabAction(
|
||||
label,
|
||||
icon,
|
||||
onScanQrCodeClick,
|
||||
label = label,
|
||||
icon = icon,
|
||||
onFabOptionClick = onScanQrCodeClick,
|
||||
)
|
||||
|
||||
/**
|
||||
* Indicates the Enter Key button was clicked.
|
||||
*/
|
||||
class EnterSetupKey(
|
||||
label: Text?,
|
||||
icon: IconResource,
|
||||
label: Text,
|
||||
icon: IconData.Local,
|
||||
onEnterSetupKeyClick: () -> Unit,
|
||||
) : ItemListingExpandableFabAction(
|
||||
label,
|
||||
icon,
|
||||
onEnterSetupKeyClick,
|
||||
label = label,
|
||||
icon = icon,
|
||||
onFabOptionClick = onEnterSetupKeyClick,
|
||||
)
|
||||
}
|
||||
|
||||
@ -10,11 +10,10 @@ 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.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
@ -24,38 +23,35 @@ 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.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.composition.LocalPermissionsManager
|
||||
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The screen to manually add a totp code.
|
||||
@ -134,7 +130,7 @@ fun ManualCodeEntryScreen(
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.create_verification_code),
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
@ -144,7 +140,7 @@ fun ManualCodeEntryScreen(
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
) {
|
||||
ManualCodeEntryContent(
|
||||
state = state,
|
||||
onNameChange = remember(viewModel) {
|
||||
@ -168,11 +164,11 @@ fun ManualCodeEntryScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues = paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ManualCodeEntryContent(
|
||||
state: ManualCodeEntryState,
|
||||
@ -184,38 +180,40 @@ private fun ManualCodeEntryContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.verticalScroll(state = rememberScrollState())) {
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.enter_key_manually),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.name),
|
||||
value = state.issuer,
|
||||
onValueChange = onNameChange,
|
||||
cardStyle = CardStyle.Top(),
|
||||
modifier = Modifier
|
||||
.testTag(tag = "NameTextField")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenPasswordField(
|
||||
singleLine = false,
|
||||
label = stringResource(id = BitwardenString.key),
|
||||
value = state.code,
|
||||
onValueChange = onKeyChange,
|
||||
capitalization = KeyboardCapitalization.Characters,
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "KeyTextField")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SaveManualCodeButtons(
|
||||
state = state.buttonState,
|
||||
onSaveLocallyClick = onSaveLocallyClick,
|
||||
@ -224,57 +222,29 @@ private fun ManualCodeEntryContent(
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.cannot_add_authenticator_key),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
ScanQrCodeText(
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.scan_qr_code),
|
||||
onClick = onScanQrCodeClick,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanQrCodeText(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val accessibilityString = stringResource(id = BitwardenString.scan_qr_code)
|
||||
Text(
|
||||
text = annotatedStringResource(
|
||||
id = BitwardenString.scan_qr_code,
|
||||
emphasisHighlightStyle = spanStyleOf(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
),
|
||||
onAnnotationClick = {
|
||||
when (it) {
|
||||
"scanQrCode" -> onClick()
|
||||
}
|
||||
},
|
||||
),
|
||||
modifier = modifier.semantics {
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction(
|
||||
label = accessibilityString,
|
||||
action = {
|
||||
onClick()
|
||||
true
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManualCodeEntryDialogs(
|
||||
dialog: ManualCodeEntryState.DialogState?,
|
||||
@ -283,16 +253,14 @@ private fun ManualCodeEntryDialogs(
|
||||
when (val dialogString = dialog) {
|
||||
is ManualCodeEntryState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialogString.title,
|
||||
message = dialogString.message,
|
||||
),
|
||||
title = dialogString.title?.invoke(),
|
||||
message = dialogString.message(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is ManualCodeEntryState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(visibilityState = LoadingDialogState.Shown(dialog.message))
|
||||
BitwardenLoadingDialog(text = dialog.message())
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorOutlinedButton
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
@ -28,7 +30,7 @@ fun SaveManualCodeButtons(
|
||||
) {
|
||||
when (state) {
|
||||
ManualCodeEntryState.ButtonState.LocalOnly -> {
|
||||
AuthenticatorFilledTonalButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.add_code),
|
||||
onClick = onSaveLocallyClick,
|
||||
modifier = modifier.testTag(tag = "AddCodeButton"),
|
||||
@ -37,12 +39,13 @@ fun SaveManualCodeButtons(
|
||||
|
||||
ManualCodeEntryState.ButtonState.SaveLocallyPrimary -> {
|
||||
Column(modifier = modifier) {
|
||||
AuthenticatorFilledButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.save_here),
|
||||
onClick = onSaveLocallyClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
AuthenticatorOutlinedButton(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(BitwardenString.save_to_bitwarden),
|
||||
onClick = onSaveToBitwardenClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -52,12 +55,13 @@ fun SaveManualCodeButtons(
|
||||
|
||||
ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary -> {
|
||||
Column(modifier = modifier) {
|
||||
AuthenticatorFilledButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.save_to_bitwarden),
|
||||
onClick = onSaveToBitwardenClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
AuthenticatorOutlinedButton(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(BitwardenString.save_here),
|
||||
onClick = onSaveLocallyClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@ -1,40 +1,17 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.navbar
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptions
|
||||
@ -46,17 +23,19 @@ import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemList
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingRoute
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.navigateToItemListGraph
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.components.scrim.BitwardenAnimatedScrim
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsGraphRoute
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsRoute
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.navigateToSettingsGraph
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.max
|
||||
import com.bitwarden.ui.platform.base.util.toDp
|
||||
import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.scaffold.model.ScaffoldNavigationData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@ -135,39 +114,30 @@ private fun AuthenticatorNavBarScaffold(
|
||||
navigateToImport: () -> Unit,
|
||||
navigateToTutorial: () -> Unit,
|
||||
) {
|
||||
var shouldDimNavBar by rememberSaveable { mutableStateOf(value = false) }
|
||||
|
||||
// This scaffold will host screens that contain top bars while not hosting one itself.
|
||||
// We need to ignore the all insets here and let the content screens handle it themselves.
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
BitwardenScaffold(
|
||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.statusBars),
|
||||
bottomBar = {
|
||||
Box {
|
||||
var appBarHeightPx by remember { mutableIntStateOf(0) }
|
||||
AuthenticatorBottomAppBar(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
appBarHeightPx = it.size.height
|
||||
},
|
||||
navController = navController,
|
||||
verificationCodesTabClickedAction = verificationTabClickedAction,
|
||||
settingsTabClickedAction = settingsTabClickedAction,
|
||||
)
|
||||
BitwardenAnimatedScrim(
|
||||
isVisible = false,
|
||||
onClick = {
|
||||
// Do nothing
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(appBarHeightPx.toDp()),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
contentWindowInsets = WindowInsets(),
|
||||
navigationData = ScaffoldNavigationData(
|
||||
navigationItems = AuthenticatorNavBarTab.navigationItems,
|
||||
selectedNavigationItem = AuthenticatorNavBarTab
|
||||
.navigationItems
|
||||
.find { navBackStackEntry.isCurrentRoute(route = it.graphRoute) },
|
||||
onNavigationClick = { navigationItem ->
|
||||
when (navigationItem) {
|
||||
AuthenticatorNavBarTab.VerificationCodes -> verificationTabClickedAction()
|
||||
AuthenticatorNavBarTab.Settings -> settingsTabClickedAction()
|
||||
}
|
||||
},
|
||||
shouldDimNavigation = shouldDimNavBar,
|
||||
),
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = ItemListingGraphRoute,
|
||||
modifier = Modifier
|
||||
.consumeWindowInsets(WindowInsets.navigationBars)
|
||||
.consumeWindowInsets(WindowInsets.ime)
|
||||
.padding(innerPadding.max(WindowInsets.ime)),
|
||||
enterTransition = RootTransitionProviders.Enter.fadeIn,
|
||||
exitTransition = RootTransitionProviders.Exit.fadeOut,
|
||||
popEnterTransition = RootTransitionProviders.Enter.fadeIn,
|
||||
@ -188,76 +158,6 @@ private fun AuthenticatorNavBarScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun AuthenticatorBottomAppBar(
|
||||
navController: NavController,
|
||||
verificationCodesTabClickedAction: () -> Unit,
|
||||
settingsTabClickedAction: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BottomAppBar(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
modifier = modifier,
|
||||
) {
|
||||
val destinations = listOf(
|
||||
AuthenticatorNavBarTab.VerificationCodes,
|
||||
AuthenticatorNavBarTab.Settings,
|
||||
)
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
destinations.forEach { destination ->
|
||||
val isSelected = currentDestination?.hierarchy?.any {
|
||||
it.route == destination.route
|
||||
} == true
|
||||
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (isSelected) {
|
||||
destination.iconResSelected
|
||||
} else {
|
||||
destination.iconRes
|
||||
},
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
id = destination.contentDescriptionRes,
|
||||
),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(id = destination.labelRes),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
when (destination) {
|
||||
AuthenticatorNavBarTab.VerificationCodes -> {
|
||||
verificationCodesTabClickedAction()
|
||||
}
|
||||
|
||||
AuthenticatorNavBarTab.Settings -> {
|
||||
settingsTabClickedAction()
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurface,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
modifier = Modifier.semantics { testTag = destination.testTag },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the different tabs available in the navigation bar
|
||||
* for the authenticator screens.
|
||||
@ -271,36 +171,17 @@ private fun AuthenticatorBottomAppBar(
|
||||
* @property iconResSelected The resource ID for the icon representing the tab when it's selected.
|
||||
*/
|
||||
@Parcelize
|
||||
private sealed class AuthenticatorNavBarTab : Parcelable {
|
||||
/**
|
||||
* The resource ID for the icon representing the tab when it is selected.
|
||||
*/
|
||||
abstract val iconResSelected: Int
|
||||
private sealed class AuthenticatorNavBarTab : NavigationItem, Parcelable {
|
||||
|
||||
/**
|
||||
* Resource id for the icon representing the tab.
|
||||
*/
|
||||
abstract val iconRes: Int
|
||||
|
||||
/**
|
||||
* Resource id for the label describing the tab.
|
||||
*/
|
||||
abstract val labelRes: Int
|
||||
|
||||
/**
|
||||
* Resource id for the content description describing the tab.
|
||||
*/
|
||||
abstract val contentDescriptionRes: Int
|
||||
|
||||
/**
|
||||
* Route of the tab.
|
||||
*/
|
||||
abstract val route: String
|
||||
|
||||
/**
|
||||
* The test tag of the tab.
|
||||
*/
|
||||
abstract val testTag: String
|
||||
companion object {
|
||||
/**
|
||||
* The list of navigation tabs available in the authenticator.
|
||||
*/
|
||||
val navigationItems: ImmutableList<AuthenticatorNavBarTab> = persistentListOf(
|
||||
VerificationCodes,
|
||||
Settings,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Verification Codes screen.
|
||||
@ -311,8 +192,10 @@ private sealed class AuthenticatorNavBarTab : Parcelable {
|
||||
override val iconRes get() = BitwardenDrawable.ic_verification_codes
|
||||
override val labelRes get() = BitwardenString.verification_codes
|
||||
override val contentDescriptionRes get() = BitwardenString.verification_codes
|
||||
override val route get() = ItemListingRoute.toObjectNavigationRoute()
|
||||
override val graphRoute get() = ItemListingGraphRoute
|
||||
override val startDestinationRoute get() = ItemListingRoute
|
||||
override val testTag get() = "VerificationCodesTab"
|
||||
override val notificationCount: Int get() = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,12 +203,14 @@ private sealed class AuthenticatorNavBarTab : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data object Settings : AuthenticatorNavBarTab() {
|
||||
override val iconResSelected get() = BitwardenDrawable.ic_settings_solid
|
||||
override val iconResSelected get() = BitwardenDrawable.ic_settings_filled
|
||||
override val iconRes get() = BitwardenDrawable.ic_settings
|
||||
override val labelRes get() = BitwardenString.settings
|
||||
override val contentDescriptionRes get() = BitwardenString.settings
|
||||
override val route get() = SettingsGraphRoute.toObjectNavigationRoute()
|
||||
override val graphRoute get() = SettingsGraphRoute
|
||||
override val startDestinationRoute get() = SettingsRoute
|
||||
override val testTag get() = "SettingsTab"
|
||||
override val notificationCount: Int get() = 0
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,3 +225,12 @@ private fun NavController.authenticatorNavBarScreenNavOptions(): NavOptions =
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current destination is the same as the given tab.
|
||||
*/
|
||||
private fun NavBackStackEntry?.isCurrentRoute(route: Any): Boolean =
|
||||
this
|
||||
?.destination
|
||||
?.parent
|
||||
?.route == route.toObjectNavigationRoute()
|
||||
|
||||
@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.layout.requiredWidthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -25,11 +23,14 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
|
||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Displays a dialog asking the user where they would like to save a new QR code.
|
||||
@ -60,8 +61,8 @@ fun ChooseSaveLocationDialog(
|
||||
max = configuration.maxDialogWidth,
|
||||
)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = BitwardenTheme.colorScheme.background.primary,
|
||||
shape = BitwardenTheme.shapes.dialog,
|
||||
),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
@ -71,8 +72,8 @@ fun ChooseSaveLocationDialog(
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(BitwardenString.verification_code_created),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
@ -81,33 +82,34 @@ fun ChooseSaveLocationDialog(
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(BitwardenString.choose_save_location_message),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
BitwardenWideSwitch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
BitwardenSwitch(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
label = stringResource(BitwardenString.save_option_as_default),
|
||||
isChecked = isSaveAsDefaultChecked,
|
||||
onCheckedChange = { isSaveAsDefaultChecked = !isSaveAsDefaultChecked },
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
) {
|
||||
AuthenticatorTextButton(
|
||||
BitwardenTextButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp),
|
||||
label = stringResource(BitwardenString.save_here),
|
||||
labelTextColor = MaterialTheme.colorScheme.primary,
|
||||
onClick = { onSaveHereClick.invoke(isSaveAsDefaultChecked) },
|
||||
)
|
||||
AuthenticatorTextButton(
|
||||
BitwardenTextButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp),
|
||||
label = stringResource(BitwardenString.save_to_bitwarden),
|
||||
labelTextColor = MaterialTheme.colorScheme.primary,
|
||||
onClick = { onTakeMeToBitwardenClick.invoke(isSaveAsDefaultChecked) },
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
@ -53,19 +52,17 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzer
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzerImpl
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.theme.LocalNonMaterialColors
|
||||
import com.bitwarden.authenticator.ui.platform.util.isPortrait
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
@ -117,7 +114,7 @@ fun QrCodeScanScreen(
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.scan_qr_code),
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
@ -127,24 +124,21 @@ fun QrCodeScanScreen(
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
) {
|
||||
CameraPreview(
|
||||
cameraErrorReceive = remember(viewModel) {
|
||||
{ viewModel.trySendAction(QrCodeScanAction.CameraSetupErrorReceive) }
|
||||
},
|
||||
qrCodeAnalyzer = qrCodeAnalyzer,
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
|
||||
if (LocalConfiguration.current.isPortrait) {
|
||||
PortraitQRCodeContent(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(paddingValues = innerPadding),
|
||||
)
|
||||
} else {
|
||||
LandscapeQRCodeContent(
|
||||
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
|
||||
modifier = Modifier.padding(paddingValues = innerPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -167,10 +161,8 @@ private fun QrCodeScanDialogs(
|
||||
|
||||
QrCodeScanState.DialogState.SaveToBitwardenError -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = BitwardenString.something_went_wrong.asText(),
|
||||
message = BitwardenString.please_try_again.asText(),
|
||||
),
|
||||
title = stringResource(id = BitwardenString.something_went_wrong),
|
||||
message = stringResource(id = BitwardenString.please_try_again),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
@ -207,7 +199,7 @@ private fun PortraitQRCodeContent(
|
||||
text = stringResource(id = BitwardenString.point_your_camera_at_the_qr_code),
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
@ -248,7 +240,7 @@ private fun LandscapeQRCodeContent(
|
||||
text = stringResource(id = BitwardenString.point_your_camera_at_the_qr_code),
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
)
|
||||
|
||||
BottomClickableText(
|
||||
@ -345,7 +337,7 @@ private fun QrCodeSquare(
|
||||
modifier: Modifier = Modifier,
|
||||
squareOutlineSize: Dp,
|
||||
) {
|
||||
val color = MaterialTheme.colorScheme.primary
|
||||
val color = BitwardenTheme.colorScheme.text.primary
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
@ -443,12 +435,12 @@ private fun BottomClickableText(
|
||||
text = annotatedStringResource(
|
||||
id = BitwardenString.cannot_scan_qr_code_enter_key_manually,
|
||||
linkHighlightStyle = spanStyleOf(
|
||||
color = LocalNonMaterialColors.current.qrCodeClickableText,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.interaction,
|
||||
textStyle = BitwardenTheme.typography.bodyMedium,
|
||||
),
|
||||
style = spanStyleOf(
|
||||
color = Color.White,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
textStyle = BitwardenTheme.typography.bodyMedium,
|
||||
),
|
||||
onAnnotationClick = {
|
||||
when (it) {
|
||||
|
||||
@ -7,8 +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.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -17,8 +16,12 @@ import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.model.VerificationCodeDisplayItem
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.search.handlers.SearchHandlers
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The content state for the item search screen.
|
||||
@ -30,32 +33,37 @@ fun ItemSearchContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
item {
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
|
||||
if (viewState.hasLocalAndSharedItems) {
|
||||
item {
|
||||
BitwardenListHeaderText(
|
||||
label = viewState.localListHeader(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
items(viewState.itemList) {
|
||||
itemsIndexed(viewState.itemList) { index, it ->
|
||||
VaultVerificationCodeItem(
|
||||
displayItem = it,
|
||||
onCopyClick = searchHandlers.onItemClick,
|
||||
cardStyle = viewState.itemList.toListItemCardStyle(index = index),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// There is some built-in padding to the menu button that
|
||||
// makes up the visual difference here.
|
||||
.padding(start = 16.dp, end = 12.dp),
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
if (viewState.hasLocalAndSharedItems) {
|
||||
item {
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,14 +91,17 @@ private fun LazyListScope.sharedCodes(
|
||||
label = section.label(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
|
||||
items(section.codes) {
|
||||
itemsIndexed(section.codes) { index, it ->
|
||||
VaultVerificationCodeItem(
|
||||
displayItem = it,
|
||||
onCopyClick = onCopyClick,
|
||||
cardStyle = section.codes.toListItemCardStyle(index = index),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// There is some built-in padding to the menu button that
|
||||
@ -105,8 +116,8 @@ private fun LazyListScope.sharedCodes(
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(BitwardenString.shared_codes_error),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
@ -117,6 +128,7 @@ private fun LazyListScope.sharedCodes(
|
||||
@Composable
|
||||
private fun VaultVerificationCodeItem(
|
||||
displayItem: VerificationCodeDisplayItem,
|
||||
cardStyle: CardStyle,
|
||||
onCopyClick: (authCode: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@ -130,6 +142,7 @@ private fun VaultVerificationCodeItem(
|
||||
startIcon = displayItem.startIcon,
|
||||
onCopyClick = { onCopyClick(displayItem.authCode) },
|
||||
onItemClick = { onCopyClick(displayItem.authCode) },
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,20 +6,19 @@ 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.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The empty state for the item search screen.
|
||||
@ -37,10 +36,10 @@ fun ItemSearchEmptyContent(
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_search_wide),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier
|
||||
.size(74.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
@ -49,11 +48,11 @@ fun ItemSearchEmptyContent(
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "NoSearchResultsLabel" }
|
||||
.testTag("NoSearchResultsLabel")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.standardHorizontalMargin(),
|
||||
text = it(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ package com.bitwarden.authenticator.ui.authenticator.feature.search
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
@ -12,18 +11,18 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.search.handlers.SearchHandlers
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorSearchTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.bottomDivider
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenSearchTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
@ -40,7 +39,7 @@ fun ItemSearchScreen(
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val searchHandlers = remember(viewModel) { SearchHandlers.create(viewModel) }
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val resources = LocalResources.current
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
@ -58,14 +57,15 @@ fun ItemSearchScreen(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorSearchTopAppBar(
|
||||
BitwardenSearchTopAppBar(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "SearchFieldEntry" }
|
||||
.testTag("SearchFieldEntry")
|
||||
.bottomDivider(),
|
||||
searchTerm = state.searchTerm,
|
||||
placeholder = stringResource(id = BitwardenString.search_codes),
|
||||
onSearchTermChange = searchHandlers.onSearchTermChange,
|
||||
scrollBehavior = scrollBehavior,
|
||||
clearIconContentDescription = stringResource(id = BitwardenString.clear),
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.back),
|
||||
@ -73,24 +73,20 @@ fun ItemSearchScreen(
|
||||
),
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
) {
|
||||
when (val viewState = state.viewState) {
|
||||
is ItemSearchState.ViewState.Content -> {
|
||||
ItemSearchContent(
|
||||
viewState = viewState,
|
||||
searchHandlers = searchHandlers,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues = innerPadding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
is ItemSearchState.ViewState.Empty -> {
|
||||
ItemSearchEmptyContent(
|
||||
viewState = viewState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues = innerPadding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.search
|
||||
|
||||
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
|
||||
@ -10,11 +8,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@ -22,12 +17,14 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.components.indicator.AuthenticatorCircularCountdownIndicator
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The verification code item displayed to the user.
|
||||
@ -53,25 +50,24 @@ fun VaultVerificationCodeItem(
|
||||
startIcon: IconData,
|
||||
onCopyClick: () -> Unit,
|
||||
onItemClick: () -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
supportingLabel: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = 60.dp)
|
||||
.cardStyle(
|
||||
cardStyle = cardStyle,
|
||||
onClick = onItemClick,
|
||||
)
|
||||
.defaultMinSize(minHeight = 72.dp)
|
||||
.padding(vertical = 8.dp)
|
||||
.then(modifier),
|
||||
paddingStart = 16.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
BitwardenIcon(
|
||||
iconData = startIcon,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
@ -83,8 +79,8 @@ fun VaultVerificationCodeItem(
|
||||
if (!issuer.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = issuer,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@ -92,15 +88,15 @@ fun VaultVerificationCodeItem(
|
||||
if (!supportingLabel.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = supportingLabel,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticatorCircularCountdownIndicator(
|
||||
BitwardenCircularCountdownIndicator(
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
@ -108,8 +104,8 @@ fun VaultVerificationCodeItem(
|
||||
|
||||
Text(
|
||||
text = authCode.chunked(3).joinToString(" "),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.sensitiveInfoSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@ -118,7 +114,7 @@ fun VaultVerificationCodeItem(
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_copy),
|
||||
contentDescription = stringResource(id = BitwardenString.copy),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
}
|
||||
@ -129,7 +125,7 @@ fun VaultVerificationCodeItem(
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun VerificationCodeItem_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenTheme {
|
||||
VaultVerificationCodeItem(
|
||||
startIcon = IconData.Local(BitwardenDrawable.ic_login_item),
|
||||
issuer = "Sample Label",
|
||||
@ -141,6 +137,7 @@ private fun VerificationCodeItem_preview() {
|
||||
onItemClick = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
alertThresholdSeconds = 7,
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.appbar
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MediumTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
|
||||
/**
|
||||
* A custom Bitwarden-themed medium top app bar with support for actions.
|
||||
*
|
||||
* This app bar wraps around Material 3's [MediumTopAppBar] and customizes its appearance
|
||||
* and behavior according to the app theme.
|
||||
* It provides a title and an optional set of actions on the trailing side.
|
||||
* These actions are arranged within a custom action row tailored to the app's design requirements.
|
||||
*
|
||||
* @param title The text to be displayed as the title of the app bar.
|
||||
* @param scrollBehavior Defines the scrolling behavior of the app bar. It controls how the app bar
|
||||
* behaves in conjunction with scrolling content.
|
||||
* @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.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AuthenticatorMediumTopAppBar(
|
||||
title: String,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
MediumTopAppBar(
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.semantics { testTag = "PageTitleLabel" },
|
||||
)
|
||||
},
|
||||
modifier = modifier.semantics { testTag = "HeaderBarComponent" },
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AuthenticatorMediumTopAppBar_preview() {
|
||||
MaterialTheme {
|
||||
AuthenticatorMediumTopAppBar(
|
||||
title = "Preview Title",
|
||||
scrollBehavior = TopAppBarDefaults
|
||||
.exitUntilCollapsedScrollBehavior(
|
||||
rememberTopAppBarState(),
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_ellipsis_vertical),
|
||||
contentDescription = "",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.appbar
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
||||
*
|
||||
* - an optional single navigation control in the upper-left defined by [navigationIcon].
|
||||
* - an editable [TextField] populated by a [searchTerm] in the middle.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AuthenticatorSearchTopAppBar(
|
||||
searchTerm: String,
|
||||
placeholder: String,
|
||||
onSearchTermChange: (String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
navigationIcon: NavigationIcon?,
|
||||
modifier: Modifier = Modifier,
|
||||
autoFocus: Boolean = true,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TopAppBar(
|
||||
modifier = modifier.semantics { testTag = "HeaderBarComponent" },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
navigationIcon?.let {
|
||||
IconButton(
|
||||
onClick = it.onNavigationIconClick,
|
||||
modifier = Modifier.semantics { testTag = "CloseButton" },
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
painter = it.navigationIcon,
|
||||
contentDescription = it.navigationIconContentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
TextField(
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
placeholder = { Text(text = placeholder) },
|
||||
value = searchTerm,
|
||||
onValueChange = onSearchTermChange,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = { onSearchTermChange("") },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_close),
|
||||
contentDescription = stringResource(id = BitwardenString.clear),
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
if (autoFocus) {
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.appbar
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
||||
*
|
||||
* - a single navigation control in the upper-left defined by [navigationIcon],
|
||||
* [navigationIconContentDescription], and [onNavigationIconClick].
|
||||
* - a [title] in the middle.
|
||||
* - a [actions] 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.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AuthenticatorTopAppBar(
|
||||
title: String,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
navigationIcon: Painter,
|
||||
navigationIconContentDescription: String,
|
||||
onNavigationIconClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable RowScope.() -> Unit = { },
|
||||
) {
|
||||
AuthenticatorTopAppBar(
|
||||
title = title,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = navigationIcon,
|
||||
navigationIconContentDescription = navigationIconContentDescription,
|
||||
onNavigationIconClick = onNavigationIconClick,
|
||||
),
|
||||
modifier = modifier,
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
||||
*
|
||||
* - an optional single navigation control in the upper-left defined by [navigationIcon].
|
||||
* - a [title] in the middle.
|
||||
* - a [actions] 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.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AuthenticatorTopAppBar(
|
||||
title: String,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
navigationIcon: NavigationIcon?,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
navigationIcon?.let {
|
||||
IconButton(
|
||||
onClick = it.onNavigationIconClick,
|
||||
modifier = Modifier.semantics { testTag = "CloseButton" },
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
painter = it.navigationIcon,
|
||||
contentDescription = it.navigationIconContentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.semantics { testTag = "PageTitleLabel" },
|
||||
)
|
||||
},
|
||||
modifier = modifier.semantics { testTag = "HeaderBarComponent" },
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AuthenticatorTopAppBar_preview() {
|
||||
AuthenticatorTheme {
|
||||
AuthenticatorTopAppBar(
|
||||
title = "Title",
|
||||
scrollBehavior = TopAppBarDefaults
|
||||
.exitUntilCollapsedScrollBehavior(
|
||||
rememberTopAppBarState(),
|
||||
),
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.appbar.action
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
|
||||
/**
|
||||
* Represents the Authenticator search action item.
|
||||
*
|
||||
* This is an [Icon] composable tailored specifically for the search functionality
|
||||
* in the Authenticator app.
|
||||
* It presents the search icon and offers an `onClick` callback for when the icon is tapped.
|
||||
*
|
||||
* @param contentDescription A description of the UI element, used for accessibility purposes.
|
||||
* @param onClick A callback to be invoked when this action item is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorSearchActionItem(
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.testTag("SearchButton"),
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ic_search_wide),
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AuthenticatorSearchActionItem_preview() {
|
||||
AuthenticatorSearchActionItem(
|
||||
contentDescription = "Search",
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.button
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden Authenticator-styled filled [Button].
|
||||
*
|
||||
* @param label The label for the button.
|
||||
* @param onClick The callback when the button is clicked.
|
||||
* @param modifier The [Modifier] to be applied to the button.
|
||||
* @param isEnabled Whether or not the button is enabled.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorFilledButton(
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.semantics(mergeDescendants = true) {},
|
||||
enabled = isEnabled,
|
||||
contentPadding = PaddingValues(
|
||||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
colors = ButtonDefaults.buttonColors(),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AuthenticatorFilledButton_preview_isEnabled() {
|
||||
AuthenticatorFilledButton(
|
||||
label = "Label",
|
||||
onClick = {},
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenFilledButton_preview_isNotEnabled() {
|
||||
AuthenticatorFilledButton(
|
||||
label = "Label",
|
||||
onClick = {},
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.button
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
|
||||
/**
|
||||
* A filled tonal button for the Bitwarden Authenticator UI with a customized appearance.
|
||||
*
|
||||
* This button uses the `secondaryContainer` color from the current [MaterialTheme.colorScheme]
|
||||
* for its background and the `onSecondaryContainer` color for its label text.
|
||||
*
|
||||
* @param label The text to be displayed on the button.
|
||||
* @param onClick A lambda which will be invoked when the button is clicked.
|
||||
* @param isEnabled Whether or not the button is enabled.
|
||||
* @param modifier A [Modifier] for this composable, allowing for adjustments to its appearance
|
||||
* or behavior. This can be used to apply padding, layout, and other Modifiers.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorFilledTonalButton(
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
contentPadding = PaddingValues(
|
||||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
enabled = isEnabled,
|
||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AuthenticatorFilledTonalButton_preview() {
|
||||
AuthenticatorTheme {
|
||||
AuthenticatorFilledTonalButton(
|
||||
label = "Sample Text",
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.button
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden Authenticator-styled filled [OutlinedButton].
|
||||
*
|
||||
* @param label The label for the button.
|
||||
* @param onClick The callback when the button is clicked.
|
||||
* @param modifier The [Modifier] to be applied to the button.
|
||||
* @param isEnabled Whether or not the button is enabled.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorOutlinedButton(
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.semantics(mergeDescendants = true) { },
|
||||
enabled = isEnabled,
|
||||
contentPadding = PaddingValues(
|
||||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AuthenticatorOutlinedButton_preview_isEnabled() {
|
||||
AuthenticatorOutlinedButton(
|
||||
label = "Label",
|
||||
onClick = {},
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AuthenticatorOutlinedButton_preview_isNotEnabled() {
|
||||
AuthenticatorOutlinedButton(
|
||||
label = "Label",
|
||||
onClick = {},
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.button
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden Authenticator-styled [TextButton].
|
||||
*
|
||||
* @param label The label for the button.
|
||||
* @param onClick The callback when the button is clicked.
|
||||
* @param modifier The [Modifier] to be applied to the button.
|
||||
* @param labelTextColor The color for the label text.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorTextButton(
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
labelTextColor: Color? = null,
|
||||
) {
|
||||
val defaultColors = if (labelTextColor != null) {
|
||||
ButtonDefaults.textButtonColors(
|
||||
contentColor = labelTextColor,
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.textButtonColors()
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = isEnabled,
|
||||
colors = defaultColors,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = 10.dp,
|
||||
horizontal = 12.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AuthenticatorTextButton_preview() {
|
||||
AuthenticatorTextButton(
|
||||
label = "Label",
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
@ -1,132 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.card
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* A reusable card for displaying actions to the user.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorActionCard(
|
||||
actionIcon: VectorPainter,
|
||||
titleText: String,
|
||||
actionText: String,
|
||||
callToActionText: String,
|
||||
onCardClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
onClick = onCardClicked,
|
||||
shape = RoundedCornerShape(size = 16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
modifier = modifier,
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
painter = actionIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 16.dp)
|
||||
.size(24.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f)
|
||||
.padding(vertical = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = titleText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = actionText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = callToActionText,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Box {
|
||||
trailingContent?.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ActionCardPreview() {
|
||||
AuthenticatorActionCard(
|
||||
actionIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
actionText = "This is an action.",
|
||||
callToActionText = "Take action",
|
||||
titleText = "This is a title",
|
||||
onCardClicked = { },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ActionCardWithTrailingPreview() {
|
||||
AuthenticatorActionCard(
|
||||
actionIcon = rememberVectorPainter(id = BitwardenDrawable.ic_shield),
|
||||
actionText = "An action with trailing content",
|
||||
titleText = "This is a title",
|
||||
callToActionText = "Take action",
|
||||
onCardClicked = {},
|
||||
trailingContent = {
|
||||
IconButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_close),
|
||||
contentDescription = stringResource(id = BitwardenString.close),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.content
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* A Bitwarden-themed, re-usable error state.
|
||||
*
|
||||
* Note that when [onTryAgainClick] is absent, there will be no "Try again" button displayed.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorErrorContent(
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onTryAgainClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
onTryAgainClick?.let {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AuthenticatorTextButton(
|
||||
label = stringResource(id = BitwardenString.try_again),
|
||||
onClick = it,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.content
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
/**
|
||||
* A Bitwarden-themed, re-usable loading state.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorLoadingContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled dialog that is hidden or shown based on [visibilityState].
|
||||
*
|
||||
* @param visibilityState the [BasicDialogState] used to populate the dialog.
|
||||
* @param onDismissRequest called when the user has requested to dismiss the dialog, whether by
|
||||
* tapping "OK", tapping outside the dialog, or pressing the back button.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenBasicDialog(
|
||||
visibilityState: BasicDialogState,
|
||||
onDismissRequest: () -> Unit,
|
||||
): Unit = when (visibilityState) {
|
||||
BasicDialogState.Hidden -> Unit
|
||||
is BasicDialogState.Shown -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
AuthenticatorTextButton(
|
||||
label = stringResource(id = BitwardenString.okay),
|
||||
onClick = onDismissRequest,
|
||||
)
|
||||
},
|
||||
title = visibilityState.title?.let {
|
||||
{
|
||||
Text(
|
||||
text = it(),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = visibilityState.message(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenBasicDialog_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = "An error has occurred".asText(),
|
||||
message = "Username or password is incorrect. Try again.".asText(),
|
||||
),
|
||||
onDismissRequest = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models display of a [BitwardenBasicDialog].
|
||||
*/
|
||||
sealed class BasicDialogState : Parcelable {
|
||||
|
||||
/**
|
||||
* Hide the dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Hidden : BasicDialogState()
|
||||
|
||||
/**
|
||||
* Show the dialog with the given values.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Shown(
|
||||
val title: Text?,
|
||||
val message: Text,
|
||||
) : BasicDialogState()
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled loading dialog that shows text and a circular progress indicator.
|
||||
*
|
||||
* @param visibilityState the [LoadingDialogState] used to populate the dialog.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenLoadingDialog(
|
||||
visibilityState: LoadingDialogState,
|
||||
) {
|
||||
when (visibilityState) {
|
||||
is LoadingDialogState.Hidden -> Unit
|
||||
is LoadingDialogState.Shown -> {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = visibilityState.text(),
|
||||
modifier = Modifier.padding(
|
||||
top = 24.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(
|
||||
top = 8.dp,
|
||||
bottom = 24.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenLoadingDialog_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = "Loading...".asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models display of a [BitwardenLoadingDialog].
|
||||
*/
|
||||
sealed class LoadingDialogState : Parcelable {
|
||||
/**
|
||||
* Hide the dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Hidden : LoadingDialogState()
|
||||
|
||||
/**
|
||||
* Show the dialog with the given values.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Shown(val text: Text) : LoadingDialogState()
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* Displays a dialog with a title and "Cancel" button.
|
||||
*
|
||||
* @param title Title to display.
|
||||
* @param subtitle Optional subtitle to display below the title.
|
||||
* @param dismissLabel Label to show on the dismiss button at the bottom of the dialog.
|
||||
* @param onDismissRequest Invoked when the user dismisses the dialog.
|
||||
* @param onDismissActionClick Invoked when the user dismisses the via the dismiss action button.
|
||||
* By default, this just defers to onDismissRequest.
|
||||
* @param selectionItems Lambda containing selection items to show to the user. See
|
||||
* [BitwardenSelectionRow].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenSelectionDialog(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
dismissLabel: String = stringResource(BitwardenString.cancel),
|
||||
onDismissRequest: () -> Unit,
|
||||
onDismissActionClick: () -> Unit = onDismissRequest,
|
||||
selectionItems: @Composable ColumnScope.() -> Unit = {},
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.semantics { testTagsAsResourceId = true }
|
||||
.requiredHeightIn(
|
||||
max = configuration.maxDialogHeight,
|
||||
)
|
||||
// This background is necessary for the dialog to not be transparent.
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(24.dp)
|
||||
.fillMaxWidth(),
|
||||
text = title,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
subtitle?.let {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 24.dp,
|
||||
end = 24.dp,
|
||||
bottom = 24.dp,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
text = subtitle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
if (scrollState.canScrollBackward) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(MaterialTheme.colorScheme.outlineVariant),
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(scrollState),
|
||||
content = selectionItems,
|
||||
)
|
||||
if (scrollState.canScrollForward) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(MaterialTheme.colorScheme.outlineVariant),
|
||||
)
|
||||
}
|
||||
AuthenticatorTextButton(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
label = dismissLabel,
|
||||
onClick = onDismissActionClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.selected
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* A clickable item that displays a radio button and text.
|
||||
*
|
||||
* @param text The text to display.
|
||||
* @param onClick Invoked when either the radio button or text is clicked.
|
||||
* @param isSelected Whether or not the radio button should be checked.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenSelectionRow(
|
||||
text: Text,
|
||||
onClick: () -> Unit,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.semantics(mergeDescendants = true) {
|
||||
selected = isSelected
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
selected = isSelected,
|
||||
onClick = null,
|
||||
)
|
||||
Text(
|
||||
text = text(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled dialog with two buttons.
|
||||
*
|
||||
* @param title the optional title to show.
|
||||
* @param message message to show.
|
||||
* @param confirmButtonText text to show on confirm button.
|
||||
* @param dismissButtonText text to show on dismiss button.
|
||||
* @param onConfirmClick called when the confirm button is clicked.
|
||||
* @param onDismissClick called when the dismiss button is clicked.
|
||||
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
|
||||
* tapping outside of it).
|
||||
* @param confirmTextColor The color of the confirm text.
|
||||
* @param dismissTextColor The color of the dismiss text.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenTwoButtonDialog(
|
||||
title: String?,
|
||||
message: String,
|
||||
confirmButtonText: String,
|
||||
dismissButtonText: String,
|
||||
onConfirmClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
confirmTextColor: Color? = null,
|
||||
dismissTextColor: Color? = null,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
AuthenticatorTextButton(
|
||||
label = dismissButtonText,
|
||||
labelTextColor = dismissTextColor,
|
||||
onClick = onDismissClick,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
AuthenticatorTextButton(
|
||||
label = confirmButtonText,
|
||||
labelTextColor = confirmTextColor,
|
||||
onClick = onConfirmClick,
|
||||
)
|
||||
},
|
||||
title = title?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
)
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog.row
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
|
||||
/**
|
||||
* A simple clickable row for use within a [BitwardenSelectionDialog] as an alternative to a
|
||||
* [BitwardenSelectionRow].
|
||||
*
|
||||
* @param text The text to display in the row.
|
||||
* @param onClick A callback to be invoked when the row is clicked.
|
||||
* @param modifier A [Modifier] for the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenBasicDialogRow(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(
|
||||
vertical = 16.dp,
|
||||
horizontal = 24.dp,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dialog.row
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.selected
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* A clickable item that displays a radio button and text.
|
||||
*
|
||||
* @param text The text to display.
|
||||
* @param onClick Invoked when either the radio button or text is clicked.
|
||||
* @param isSelected Whether or not the radio button should be checked.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenSelectionRow(
|
||||
text: Text,
|
||||
onClick: () -> Unit,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.semantics(mergeDescendants = true) {
|
||||
selected = isSelected
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
selected = isSelected,
|
||||
onClick = null,
|
||||
)
|
||||
Text(
|
||||
text = text(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.dropdown
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionRow
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.components.model.TooltipData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.util.asText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* A custom composable representing a multi-select button.
|
||||
*
|
||||
* This composable displays an [OutlinedTextField] with a dropdown icon as a trailing icon.
|
||||
* When the field is clicked, a dropdown menu appears with a list of options to select from.
|
||||
*
|
||||
* @param label The descriptive text label for the [OutlinedTextField].
|
||||
* @param options A list of strings representing the available options in the dialog.
|
||||
* @param selectedOption The currently selected option that is displayed in the [OutlinedTextField]
|
||||
* (or `null` if no option is selected).
|
||||
* @param onOptionSelected A lambda that is invoked when an option
|
||||
* is selected from the dropdown menu.
|
||||
* @param isEnabled Whether or not the button is enabled.
|
||||
* @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.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenMultiSelectButton(
|
||||
label: String,
|
||||
options: ImmutableList<String>,
|
||||
selectedOption: String?,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
supportingText: String? = null,
|
||||
tooltip: TooltipData? = null,
|
||||
) {
|
||||
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.clearAndSetSemantics {
|
||||
role = Role.DropdownList
|
||||
contentDescription = supportingText
|
||||
?.let { "$selectedOption. $label. $it" }
|
||||
?: "$selectedOption. $label"
|
||||
customActions = listOfNotNull(
|
||||
tooltip?.let {
|
||||
CustomAccessibilityAction(
|
||||
label = it.contentDescription,
|
||||
action = {
|
||||
it.onClick()
|
||||
true
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
enabled = isEnabled,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
shouldShowDialog = !shouldShowDialog
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
readOnly = true,
|
||||
label = {
|
||||
Row {
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
tooltip?.let {
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
IconButton(
|
||||
onClick = it.onClick,
|
||||
enabled = isEnabled,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
modifier = Modifier.size(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_tooltip_small),
|
||||
contentDescription = it.contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
value = selectedOption.orEmpty(),
|
||||
onValueChange = onOptionSelected,
|
||||
enabled = shouldShowDialog,
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_region_select_dropdown),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outline,
|
||||
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledSupportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
supportingText = supportingText?.let {
|
||||
{
|
||||
Text(
|
||||
text = supportingText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (shouldShowDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = label,
|
||||
onDismissRequest = { shouldShowDialog = false },
|
||||
) {
|
||||
options.forEach { optionString ->
|
||||
BitwardenSelectionRow(
|
||||
text = optionString.asText(),
|
||||
isSelected = optionString == selectedOption,
|
||||
onClick = {
|
||||
shouldShowDialog = false
|
||||
onOptionSelected(optionString)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenMultiSelectButton_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenMultiSelectButton(
|
||||
label = "Label",
|
||||
options = persistentListOf("a", "b"),
|
||||
selectedOption = "",
|
||||
onOptionSelected = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,210 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.fab
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.authenticator.ui.platform.theme.Typography
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* A FAB that expands, when clicked, to display a collection of options that can be clicked.
|
||||
*
|
||||
* @param label [Text] displayed when the FAB is expanded.
|
||||
* @param items [ExpandableFabOption] buttons displayed when the FAB is expanded.
|
||||
* @param expandableFabState [ExpandableFabIcon] displayed in the FAB.
|
||||
* @param onStateChange Lambda invoked when the FAB expanded state changes.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun <T : ExpandableFabOption> ExpandableFloatingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
label: Text?,
|
||||
items: List<T>,
|
||||
expandableFabState: MutableState<ExpandableFabState> = rememberExpandableFabState(),
|
||||
expandableFabIcon: ExpandableFabIcon,
|
||||
onStateChange: (expandableFabState: ExpandableFabState) -> Unit = { },
|
||||
) {
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (expandableFabState.value == ExpandableFabState.Expanded) {
|
||||
expandableFabIcon.iconRotation ?: 0f
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
label = stringResource(BitwardenString.add_item_rotation),
|
||||
)
|
||||
Column(
|
||||
modifier = modifier.wrapContentSize(),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = expandableFabState.value.isExpanded(),
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items) { expandableFabOption ->
|
||||
ExpandableFabOption(
|
||||
onFabOptionClick = {
|
||||
expandableFabState.value = expandableFabState.value.toggleValue()
|
||||
onStateChange(expandableFabState.value)
|
||||
expandableFabOption.onFabOptionClick()
|
||||
},
|
||||
expandableFabOption = expandableFabOption,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
expandableFabState.value = expandableFabState.value.toggleValue()
|
||||
onStateChange(expandableFabState.value)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
) {
|
||||
|
||||
if (label != null) {
|
||||
AnimatedVisibility(
|
||||
visible = expandableFabState.value.isExpanded(),
|
||||
enter = fadeIn() + expandHorizontally(),
|
||||
exit = fadeOut() + shrinkHorizontally(),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
text = label(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.semantics { expandableFabIcon.iconData.testTag },
|
||||
painter = expandableFabIcon.iconData.iconPainter,
|
||||
contentDescription = expandableFabIcon.iconData.contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : ExpandableFabOption> ExpandableFabOption(
|
||||
expandableFabOption: T,
|
||||
onFabOptionClick: (option: T) -> Unit,
|
||||
) {
|
||||
SmallFloatingActionButton(
|
||||
onClick = { onFabOptionClick(expandableFabOption) },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(end = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
expandableFabOption.label?.let { label ->
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(size = 8.dp))
|
||||
.padding(all = 8.dp),
|
||||
text = label(),
|
||||
style = Typography.labelSmall,
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = expandableFabOption.iconData.iconPainter,
|
||||
contentDescription = expandableFabOption.iconData.contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberExpandableFabState() =
|
||||
remember { mutableStateOf<ExpandableFabState>(ExpandableFabState.Collapsed) }
|
||||
|
||||
/**
|
||||
* Represents options displayed when the FAB is expanded.
|
||||
*/
|
||||
abstract class ExpandableFabOption(
|
||||
val label: Text?,
|
||||
val iconData: IconResource,
|
||||
val onFabOptionClick: () -> Unit,
|
||||
)
|
||||
|
||||
/**
|
||||
* Models data for an expandable FAB icon.
|
||||
*/
|
||||
data class ExpandableFabIcon(
|
||||
val iconData: IconResource,
|
||||
val iconRotation: Float?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Models the state of the expandable FAB.
|
||||
*/
|
||||
sealed class ExpandableFabState {
|
||||
|
||||
/**
|
||||
* Indicates if the FAB is expanded.
|
||||
*/
|
||||
fun isExpanded() = this is Expanded
|
||||
|
||||
/**
|
||||
* Invert the state of the FAB.
|
||||
*/
|
||||
fun toggleValue() = if (isExpanded()) {
|
||||
Collapsed
|
||||
} else {
|
||||
Expanded
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the FAB is collapsed.
|
||||
*/
|
||||
data object Collapsed : ExpandableFabState()
|
||||
|
||||
/**
|
||||
* Indicates the FAB is expanded.
|
||||
*/
|
||||
data object Expanded : ExpandableFabState()
|
||||
}
|
||||
@ -1,235 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.field
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
* @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.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenPasswordField(
|
||||
label: String,
|
||||
value: String,
|
||||
showPassword: Boolean,
|
||||
showPasswordChange: (Boolean) -> Unit,
|
||||
onValueChange: (String) -> 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,
|
||||
capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
OutlinedTextField(
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
label = { Text(text = label) },
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
visualTransformation = when {
|
||||
!showPassword -> PasswordVisualTransformation()
|
||||
readOnly -> nonLetterColorVisualTransformation(
|
||||
digitColor = MaterialTheme.colorScheme.primary,
|
||||
specialCharacterColor = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
|
||||
else -> VisualTransformation.None
|
||||
},
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = capitalization,
|
||||
keyboardType = keyboardType,
|
||||
imeAction = imeAction,
|
||||
),
|
||||
supportingText = hint?.let {
|
||||
{
|
||||
Text(
|
||||
text = hint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = { showPasswordChange.invoke(!showPassword) },
|
||||
) {
|
||||
@DrawableRes
|
||||
val painterRes = if (showPassword) {
|
||||
BitwardenDrawable.ic_visibility_off
|
||||
} else {
|
||||
BitwardenDrawable.ic_visibility
|
||||
}
|
||||
|
||||
@StringRes
|
||||
val contentDescriptionRes =
|
||||
if (showPassword) BitwardenString.hide else BitwardenString.show
|
||||
Icon(
|
||||
modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } },
|
||||
painter = painterResource(id = painterRes),
|
||||
contentDescription = stringResource(id = contentDescriptionRes),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (autoFocus) {
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
|
||||
* internally.
|
||||
*
|
||||
* @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 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 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).
|
||||
* @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.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenPasswordField(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
hint: String? = null,
|
||||
initialShowPassword: Boolean = false,
|
||||
showPasswordTestTag: String? = null,
|
||||
autoFocus: Boolean = false,
|
||||
keyboardType: KeyboardType = KeyboardType.Password,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
|
||||
) {
|
||||
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
|
||||
BitwardenPasswordField(
|
||||
modifier = modifier,
|
||||
label = label,
|
||||
value = value,
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = !showPassword },
|
||||
onValueChange = onValueChange,
|
||||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
hint = hint,
|
||||
showPasswordTestTag = showPasswordTestTag,
|
||||
autoFocus = autoFocus,
|
||||
keyboardType = keyboardType,
|
||||
imeAction = imeAction,
|
||||
capitalization = capitalization,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenPasswordField_preview_withInput_hidePassword() {
|
||||
BitwardenPasswordField(
|
||||
label = "Label",
|
||||
value = "Password",
|
||||
onValueChange = {},
|
||||
initialShowPassword = false,
|
||||
hint = "Hint",
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenPasswordField_preview_withInput_showPassword() {
|
||||
BitwardenPasswordField(
|
||||
label = "Label",
|
||||
value = "Password",
|
||||
onValueChange = {},
|
||||
initialShowPassword = true,
|
||||
hint = "Hint",
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenPasswordField_preview_withoutInput_hidePassword() {
|
||||
BitwardenPasswordField(
|
||||
label = "Label",
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
initialShowPassword = false,
|
||||
hint = "Hint",
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenPasswordField_preview_withoutInput_showPassword() {
|
||||
BitwardenPasswordField(
|
||||
label = "Label",
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
initialShowPassword = true,
|
||||
hint = "Hint",
|
||||
)
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.field
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
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.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.ui.platform.base.util.toPx
|
||||
import com.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
|
||||
|
||||
/**
|
||||
* 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 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 hint optional hint 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.
|
||||
* @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.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenTextField(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
placeholder: String? = null,
|
||||
leadingIconResource: IconResource? = null,
|
||||
trailingIconContent: (@Composable () -> Unit)? = null,
|
||||
hint: String? = null,
|
||||
singleLine: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
textStyle: TextStyle? = null,
|
||||
shouldAddCustomLineBreaks: Boolean = false,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
) {
|
||||
var widthPx by remember { mutableIntStateOf(0) }
|
||||
|
||||
val currentTextStyle = textStyle ?: LocalTextStyle.current
|
||||
val formattedText = if (shouldAddCustomLineBreaks) {
|
||||
value.withLineBreaksAtWidth(
|
||||
// Adjust for built in padding
|
||||
widthPx = widthPx - 16.dp.toPx(),
|
||||
monospacedTextStyle = currentTextStyle,
|
||||
)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.onGloballyPositioned { widthPx = it.size.width },
|
||||
enabled = enabled,
|
||||
label = { Text(text = label) },
|
||||
value = formattedText,
|
||||
leadingIcon = leadingIconResource?.let { iconResource ->
|
||||
{
|
||||
Icon(
|
||||
painter = iconResource.iconPainter,
|
||||
contentDescription = iconResource.contentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = trailingIconContent?.let {
|
||||
trailingIconContent
|
||||
},
|
||||
placeholder = placeholder?.let {
|
||||
{ Text(text = it) }
|
||||
},
|
||||
supportingText = hint?.let {
|
||||
{
|
||||
Text(
|
||||
text = hint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
textStyle = currentTextStyle,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
|
||||
isError = isError,
|
||||
visualTransformation = visualTransformation,
|
||||
)
|
||||
}
|
||||
|
||||
@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",
|
||||
)
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.field
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
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 com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.components.row.BitwardenRowOfActions
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled text field accompanied by a series of actions.
|
||||
* This component allows for a more versatile design by accepting
|
||||
* icons or actions next to the text field.
|
||||
*
|
||||
* @param label Label for the text field.
|
||||
* @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 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 trailingIconContent the content for the trailing icon in the text field.
|
||||
* @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 textFieldTestTag The test tag to be used on the text field.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenTextFieldWithActions(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle? = null,
|
||||
shouldAddCustomLineBreaks: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
trailingIconContent: (@Composable () -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
actionsTestTag: String? = null,
|
||||
textFieldTestTag: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.semantics(mergeDescendants = true) { },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
BitwardenTextField(
|
||||
modifier = Modifier
|
||||
.semantics { textFieldTestTag?.let { testTag = it } }
|
||||
.weight(1f),
|
||||
label = label,
|
||||
value = value,
|
||||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
onValueChange = onValueChange,
|
||||
keyboardType = keyboardType,
|
||||
trailingIconContent = trailingIconContent,
|
||||
textStyle = textStyle,
|
||||
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
|
||||
visualTransformation = visualTransformation,
|
||||
)
|
||||
BitwardenRowOfActions(
|
||||
modifier = Modifier.run { actionsTestTag?.let { semantics { testTag = it } } ?: this },
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenTextFieldWithActions_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenTextFieldWithActions(
|
||||
label = "Username",
|
||||
value = "user@example.com",
|
||||
onValueChange = {},
|
||||
actions = {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_tooltip),
|
||||
contentDescription = "Action 1",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -22,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* A header that can be expanded and collapsed.
|
||||
@ -54,15 +54,15 @@ fun AuthenticatorExpandingHeader(
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(width = 8.dp))
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_up_small),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.rotate(degrees = iconRotationDegrees.value),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.header
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled label text.
|
||||
*
|
||||
* @param label The text content for the label.
|
||||
* @param modifier The [Modifier] to be applied to the label.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenListHeaderText(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = modifier.padding(
|
||||
top = 12.dp,
|
||||
bottom = 4.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenListHeaderText_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenListHeaderText(
|
||||
label = "Sample Label",
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.header
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled label text.
|
||||
*
|
||||
* @param label The text content for the label.
|
||||
* @param supportingLabel The text for the supporting label.
|
||||
* @param modifier The [Modifier] to be applied to the label.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenListHeaderTextWithSupportLabel(
|
||||
label: String,
|
||||
supportingLabel: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
top = 12.dp,
|
||||
bottom = 4.dp,
|
||||
end = 8.dp,
|
||||
)
|
||||
.semantics(mergeDescendants = true) { },
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = supportingLabel,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenListHeaderTextWithSupportLabel_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
label = "Sample Label",
|
||||
supportingLabel = "0",
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.icon
|
||||
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
|
||||
/**
|
||||
* An icon button that displays an icon from the provided [IconResource].
|
||||
*
|
||||
* @param iconRes Icon to display on the button.
|
||||
* @param onClick Callback for when the icon button is clicked.
|
||||
* @param isEnabled Whether or not the button should be enabled.
|
||||
* @param modifier A [Modifier] for the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenIconButtonWithResource(
|
||||
iconRes: IconResource,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
FilledIconButton(
|
||||
modifier = modifier.semantics(mergeDescendants = true) {},
|
||||
onClick = onClick,
|
||||
colors = IconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .12f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
enabled = isEnabled,
|
||||
) {
|
||||
Icon(
|
||||
painter = iconRes.iconPainter,
|
||||
contentDescription = iconRes.contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenIconButtonWithResource_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
iconPainter = painterResource(id = BitwardenDrawable.ic_tooltip),
|
||||
contentDescription = "Sample Icon",
|
||||
),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.indicator
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* A countdown timer displayed to the user.
|
||||
*
|
||||
* @param timeLeftSeconds The seconds left on the timer.
|
||||
* @param periodSeconds The period for the timer countdown.
|
||||
* @param modifier A [Modifier] for the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorCircularCountdownIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
timeLeftSeconds: Int,
|
||||
periodSeconds: Int,
|
||||
alertThresholdSeconds: Int = -1,
|
||||
alertIndicatorColor: Color = MaterialTheme.colorScheme.error,
|
||||
) {
|
||||
val progressAnimate by animateFloatAsState(
|
||||
targetValue = timeLeftSeconds.toFloat() / periodSeconds,
|
||||
animationSpec = tween(
|
||||
durationMillis = periodSeconds,
|
||||
delayMillis = 0,
|
||||
easing = LinearOutSlowInEasing,
|
||||
),
|
||||
label = "CircularCountDownAnimation",
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
progress = { progressAnimate },
|
||||
modifier = Modifier.size(size = 30.dp),
|
||||
color = if (timeLeftSeconds > alertThresholdSeconds) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
alertIndicatorColor
|
||||
},
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = timeLeftSeconds.toString(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (timeLeftSeconds > alertThresholdSeconds) {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
} else {
|
||||
alertIndicatorColor
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,189 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.listitem
|
||||
|
||||
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.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
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
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.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.row.BitwardenBasicDialogRow
|
||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* A Composable function that displays a row item.
|
||||
*
|
||||
* @param label The primary text label to display for the item.
|
||||
* @param startIcon The [Painter] object used to draw the icon at the start of the item.
|
||||
* @param onClick The lambda to be invoked when the item is clicked.
|
||||
* @param selectionDataList A list of all the selection items to be displayed in the overflow
|
||||
* dialog.
|
||||
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
|
||||
* This allows the caller to specify things like padding, size, etc.
|
||||
* @param labelTestTag The optional test tag for the [label].
|
||||
* @param optionsTestTag The optional test tag for the options button.
|
||||
* @param supportingLabel An optional secondary text label to display beneath the label.
|
||||
* @param supportingLabelTestTag The optional test tag for the [supportingLabel].
|
||||
* @param trailingLabelIcons An optional list of small icons to be displayed after the [label].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenListItem(
|
||||
label: String,
|
||||
startIcon: IconData,
|
||||
onClick: () -> Unit,
|
||||
selectionDataList: ImmutableList<SelectionItemData>,
|
||||
modifier: Modifier = Modifier,
|
||||
labelTestTag: String? = null,
|
||||
optionsTestTag: String? = null,
|
||||
supportingLabel: String? = null,
|
||||
supportingLabelTestTag: String? = null,
|
||||
trailingLabelIcons: ImmutableList<IconResource> = persistentListOf(),
|
||||
) {
|
||||
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.defaultMinSize(minHeight = 72.dp)
|
||||
.padding(vertical = 8.dp)
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
BitwardenIcon(
|
||||
iconData = startIcon,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.semantics { labelTestTag?.let { testTag = it } }
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
|
||||
trailingLabelIcons.forEach { iconResource ->
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
painter = iconResource.iconPainter,
|
||||
contentDescription = iconResource.contentDescription,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.semantics { iconResource.testTag?.let { testTag = it } }
|
||||
.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
supportingLabel?.let { supportLabel ->
|
||||
Text(
|
||||
text = supportLabel,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.semantics { supportingLabelTestTag?.let { testTag = it } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionDataList.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { shouldShowDialog = true },
|
||||
modifier = Modifier.semantics { optionsTestTag?.let { testTag = it } },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = BitwardenDrawable.ic_more_horizontal),
|
||||
contentDescription = stringResource(id = BitwardenString.options),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = label,
|
||||
onDismissRequest = { shouldShowDialog = false },
|
||||
selectionItems = {
|
||||
selectionDataList.forEach { itemData ->
|
||||
BitwardenBasicDialogRow(
|
||||
modifier = Modifier.semantics { itemData.testTag?.let { testTag = it } },
|
||||
text = itemData.text,
|
||||
onClick = {
|
||||
shouldShowDialog = false
|
||||
itemData.onClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for the an individual selection item's data.
|
||||
*/
|
||||
data class SelectionItemData(
|
||||
val text: String,
|
||||
val onClick: () -> Unit,
|
||||
val testTag: String? = null,
|
||||
)
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenListItem_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenListItem(
|
||||
label = "Sample Label",
|
||||
supportingLabel = "Jan 3, 2024, 10:35 AM",
|
||||
startIcon = IconData.Local(BitwardenDrawable.ic_login_item),
|
||||
onClick = {},
|
||||
selectionDataList = persistentListOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.model
|
||||
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
|
||||
/**
|
||||
* Data class representing the resources required for an icon.
|
||||
*
|
||||
* @property iconPainter Painter for the icon.
|
||||
* @property contentDescription String for the icon's content description.
|
||||
* @property testTag The optional test tag to associate with this icon.
|
||||
*/
|
||||
data class IconResource(
|
||||
val iconPainter: Painter,
|
||||
val contentDescription: String,
|
||||
val testTag: String? = null,
|
||||
)
|
||||
@ -1,89 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.row
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* Represents a row of text that can be clicked on and contains an external link.
|
||||
* A confirmation dialog will always be displayed before [onConfirmClick] is invoked.
|
||||
*
|
||||
* @param text The label for the row as a [String].
|
||||
* @param onConfirmClick The callback when the confirm button of the dialog is clicked.
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param withDivider Indicates if a divider should be drawn on the bottom of the row, defaults
|
||||
* to `true`.
|
||||
* @param dialogTitle The title of the dialog displayed when the user clicks this item.
|
||||
* @param dialogMessage The message of the dialog displayed when the user clicks this item.
|
||||
* @param dialogConfirmButtonText The text on the confirm button of the dialog displayed when the
|
||||
* user clicks this item.
|
||||
* @param dialogDismissButtonText The text on the dismiss button of the dialog displayed when the
|
||||
* user clicks this item.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenExternalLinkRow(
|
||||
text: String,
|
||||
onConfirmClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
withDivider: Boolean = true,
|
||||
dialogTitle: String,
|
||||
dialogMessage: String,
|
||||
dialogConfirmButtonText: String = stringResource(id = BitwardenString.continue_text),
|
||||
dialogDismissButtonText: String = stringResource(id = BitwardenString.cancel),
|
||||
) {
|
||||
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenTextRow(
|
||||
text = text,
|
||||
onClick = { shouldShowDialog = true },
|
||||
modifier = modifier,
|
||||
withDivider = withDivider,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ic_external_link),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialogTitle,
|
||||
message = dialogMessage,
|
||||
confirmButtonText = dialogConfirmButtonText,
|
||||
dismissButtonText = dialogDismissButtonText,
|
||||
onConfirmClick = {
|
||||
shouldShowDialog = false
|
||||
onConfirmClick()
|
||||
},
|
||||
onDismissClick = { shouldShowDialog = false },
|
||||
onDismissRequest = { shouldShowDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenExternalLinkRow_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenExternalLinkRow(
|
||||
text = "Linked Text",
|
||||
onConfirmClick = { },
|
||||
dialogTitle = "",
|
||||
dialogMessage = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.row
|
||||
|
||||
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.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a clickable row of text and can contains an optional [content] that appears to the
|
||||
* right of the [text].
|
||||
*
|
||||
* @param text The label for the row as a [String].
|
||||
* @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 withDivider Indicates if a divider should be drawn on the bottom of the row, defaults
|
||||
* to `false`.
|
||||
* @param content The content of the [BitwardenTextRow].
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenTextRow(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
description: AnnotatedString? = null,
|
||||
withDivider: Boolean = false,
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.semantics(mergeDescendants = true) { },
|
||||
) {
|
||||
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,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
content?.invoke()
|
||||
}
|
||||
if (withDivider) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.scaffold
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
|
||||
/**
|
||||
* Direct passthrough to [Scaffold] but contains a few specific override values. Everything is
|
||||
* still overridable if necessary.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
topBar: @Composable () -> Unit = { },
|
||||
bottomBar: @Composable () -> Unit = { },
|
||||
snackbarHost: @Composable () -> Unit = { },
|
||||
floatingActionButton: @Composable () -> Unit = { },
|
||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.exclude(WindowInsets.navigationBars),
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.semantics { testTagsAsResourceId = true }
|
||||
.then(modifier),
|
||||
topBar = topBar,
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
contentWindowInsets = contentWindowInsets,
|
||||
content = { paddingValues ->
|
||||
Box {
|
||||
content(paddingValues)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.scrim
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* A scrim that animates its visibility.
|
||||
*
|
||||
* @param isVisible Whether or not the scrim should be visible. This controls the animation.
|
||||
* @param onClick A callback that is triggered when the scrim is clicked. No ripple will be
|
||||
* performed.
|
||||
* @param modifier A [Modifier] for the scrim's content.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenAnimatedScrim(
|
||||
isVisible: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(Color.Black.copy(alpha = 0.40f))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
// Clear the ripple
|
||||
indication = null,
|
||||
onClick = onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.stepper
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenTextFieldWithActions
|
||||
import com.bitwarden.authenticator.ui.platform.components.icon.BitwardenIconButtonWithResource
|
||||
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.bitwarden.ui.platform.base.util.ZERO_WIDTH_CHARACTER
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param textFieldReadOnly whether or not the text field should be read only. The stepper
|
||||
* increment and decrement buttons function regardless of this value.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@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,
|
||||
textFieldReadOnly: Boolean = true,
|
||||
stepperActionsTestTag: String? = null,
|
||||
increaseButtonTestTag: String? = null,
|
||||
decreaseButtonTestTag: String? = null,
|
||||
) {
|
||||
val clampedValue = value?.coerceIn(range)
|
||||
if (clampedValue != value && clampedValue != null) {
|
||||
onValueChange(clampedValue)
|
||||
}
|
||||
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 = {
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
iconPainter = rememberVectorPainter(id = BitwardenDrawable.ic_minus),
|
||||
contentDescription = "\u2212",
|
||||
),
|
||||
onClick = {
|
||||
val decrementedValue = ((value ?: 0) - 1).coerceIn(range)
|
||||
if (decrementedValue != value) {
|
||||
onValueChange(decrementedValue)
|
||||
}
|
||||
},
|
||||
isEnabled = isDecrementEnabled,
|
||||
modifier = Modifier.semantics {
|
||||
if (decreaseButtonTestTag != null) {
|
||||
testTag = decreaseButtonTestTag
|
||||
}
|
||||
},
|
||||
)
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
iconPainter = rememberVectorPainter(id = BitwardenDrawable.ic_plus),
|
||||
contentDescription = "+",
|
||||
),
|
||||
onClick = {
|
||||
val incrementedValue = ((value ?: 0) + 1).coerceIn(range)
|
||||
if (incrementedValue != value) {
|
||||
onValueChange(incrementedValue)
|
||||
}
|
||||
},
|
||||
isEnabled = isIncrementEnabled,
|
||||
modifier = Modifier.semantics {
|
||||
if (increaseButtonTestTag != null) {
|
||||
testTag = increaseButtonTestTag
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
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) ?: value }
|
||||
?: range.start,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
package com.bitwarden.authenticator.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.Row
|
||||
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.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.toggleableState
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled [Switch].
|
||||
*
|
||||
* @param label The label for the switch.
|
||||
* @param isChecked Whether or not the switch is currently checked.
|
||||
* @param onCheckedChange A callback for when the checked state changes.
|
||||
* @param modifier The [Modifier] to be applied to the button.
|
||||
* @param description The description of the switch to be displayed below the [label].
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenSwitch(
|
||||
label: String,
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: ((Boolean) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
description: String? = null,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.run {
|
||||
if (onCheckedChange != null) {
|
||||
this.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = { onCheckedChange.invoke(!isChecked) },
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
.semantics(mergeDescendants = true) {
|
||||
toggleableState = ToggleableState(isChecked)
|
||||
}
|
||||
.then(modifier),
|
||||
) {
|
||||
Switch(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.height(32.dp)
|
||||
.width(52.dp),
|
||||
checked = isChecked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenSwitch_preview_isChecked() {
|
||||
BitwardenSwitch(
|
||||
label = "Label",
|
||||
description = "Description",
|
||||
isChecked = true,
|
||||
onCheckedChange = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenSwitch_preview_isNotChecked() {
|
||||
BitwardenSwitch(
|
||||
label = "Label",
|
||||
isChecked = false,
|
||||
onCheckedChange = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
@ -1,130 +0,0 @@
|
||||
package com.bitwarden.authenticator.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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.toggleableState
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
|
||||
/**
|
||||
* A wide custom switch composable
|
||||
*
|
||||
* @param label The descriptive text label to be displayed adjacent to the switch.
|
||||
* @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 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.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenWideSwitch(
|
||||
label: String,
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: ((Boolean) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
description: String? = null,
|
||||
contentDescription: String? = null,
|
||||
readOnly: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = { onCheckedChange?.invoke(!isChecked) },
|
||||
enabled = !readOnly && enabled,
|
||||
)
|
||||
.semantics(mergeDescendants = true) {
|
||||
toggleableState = ToggleableState(isChecked)
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(modifier),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (enabled) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
},
|
||||
)
|
||||
description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (enabled) {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Switch(
|
||||
modifier = Modifier
|
||||
.height(56.dp),
|
||||
checked = isChecked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenWideSwitch_preview_isChecked() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenWideSwitch(
|
||||
label = "Label",
|
||||
isChecked = true,
|
||||
onCheckedChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenWideSwitch_preview_isNotChecked() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenWideSwitch(
|
||||
label = "Label",
|
||||
isChecked = false,
|
||||
onCheckedChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,11 @@ 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
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -21,21 +21,21 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorErrorContent
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.components.ListItemContent
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.bitwarden.ui.platform.components.debug.ListItemContent
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Top level screen for the debug menu.
|
||||
@ -60,7 +60,7 @@ fun DebugMenuScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(BitwardenString.debug_menu),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = NavigationIcon(
|
||||
@ -74,13 +74,11 @@ fun DebugMenuScreen(
|
||||
),
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
) {
|
||||
if (state.featureFlags.isEmpty()) {
|
||||
AuthenticatorErrorContent(
|
||||
BitwardenErrorContent(
|
||||
message = stringResource(id = BitwardenString.empty_item_list),
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = innerPadding)
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
FeatureFlagContent(
|
||||
@ -94,8 +92,7 @@ fun DebugMenuScreen(
|
||||
{ viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(paddingValues = innerPadding),
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -111,29 +108,26 @@ private fun FeatureFlagContent(
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(BitwardenString.feature_flags),
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenHorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
thickness = 1.dp,
|
||||
)
|
||||
featureFlagMap.forEach { featureFlag ->
|
||||
featureFlagMap.onEach { featureFlag ->
|
||||
featureFlag.key.ListItemContent(
|
||||
currentValue = featureFlag.value,
|
||||
onValueChange = onValueChange,
|
||||
cardStyle = featureFlagMap.keys.toListItemCardStyle(
|
||||
index = featureFlagMap.keys.indexOf(element = featureFlag.key),
|
||||
),
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenHorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
thickness = 1.dp,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
AuthenticatorFilledButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.reset_values),
|
||||
onClick = onResetValues,
|
||||
modifier = Modifier
|
||||
@ -141,13 +135,14 @@ private fun FeatureFlagContent(
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FeatureFlagContent_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenTheme {
|
||||
FeatureFlagContent(
|
||||
featureFlagMap = mapOf(
|
||||
FlagKey.BitwardenAuthenticationEnabled to true,
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* Creates a list item for a [FlagKey].
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
currentValue: T,
|
||||
onValueChange: (key: FlagKey<T>, value: T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) = when (val flagKey = this) {
|
||||
is FlagKey.DummyInt,
|
||||
FlagKey.DummyString,
|
||||
-> Unit
|
||||
|
||||
FlagKey.DummyBoolean,
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
-> BooleanFlagItem(
|
||||
label = flagKey.getDisplayLabel(),
|
||||
key = flagKey as FlagKey<Boolean>,
|
||||
currentValue = currentValue as Boolean,
|
||||
onValueChange = onValueChange as (FlagKey<Boolean>, Boolean) -> Unit,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The UI layout for a boolean backed flag key.
|
||||
*/
|
||||
@Composable
|
||||
private fun BooleanFlagItem(
|
||||
label: String,
|
||||
key: FlagKey<Boolean>,
|
||||
currentValue: Boolean,
|
||||
onValueChange: (key: FlagKey<Boolean>, value: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenWideSwitch(
|
||||
label = label,
|
||||
isChecked = currentValue,
|
||||
onCheckedChange = {
|
||||
onValueChange(key, it)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.DummyBoolean,
|
||||
is FlagKey.DummyInt,
|
||||
FlagKey.DummyString,
|
||||
-> this.keyName
|
||||
|
||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
||||
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
||||
FlagKey.BitwardenAuthenticationEnabled -> {
|
||||
stringResource(BitwardenString.bitwarden_authentication_enabled)
|
||||
}
|
||||
}
|
||||
@ -4,29 +4,24 @@ package com.bitwarden.authenticator.ui.platform.feature.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
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.ColumnScope
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -42,38 +37,39 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorMediumTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionRow
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.authenticator.ui.platform.components.row.BitwardenExternalLinkRow
|
||||
import com.bitwarden.authenticator.ui.platform.components.row.BitwardenTextRow
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.bitwarden.authenticator.ui.platform.composition.LocalBiometricsManager
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.authenticator.ui.platform.util.displayLabel
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
import com.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
||||
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
|
||||
import com.bitwarden.ui.platform.components.row.BitwardenTextRow
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.displayLabel
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Display the settings screen.
|
||||
@ -90,8 +86,7 @@ fun SettingsScreen(
|
||||
onNavigateToImport: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val scrollBehavior =
|
||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
@ -141,15 +136,14 @@ fun SettingsScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorMediumTopAppBar(
|
||||
BitwardenMediumTopAppBar(
|
||||
title = stringResource(id = BitwardenString.settings),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
) {
|
||||
@ -245,10 +239,12 @@ fun SettingsScreen(
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = state.copyrightInfo.invoke(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -262,9 +258,11 @@ private fun SecuritySettings(
|
||||
onBiometricToggle: (Boolean) -> Unit,
|
||||
) {
|
||||
if (!biometricsManager.isBiometricsSupported) return
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = BitwardenString.security),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@ -272,7 +270,7 @@ private fun SecuritySettings(
|
||||
modifier = Modifier
|
||||
.testTag("UnlockWithBiometricsSwitch")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.standardHorizontalMargin(),
|
||||
isChecked = state.isUnlockWithBiometricsEnabled,
|
||||
onBiometricToggle = { onBiometricToggle(it) },
|
||||
biometricsManager = biometricsManager,
|
||||
@ -285,8 +283,7 @@ private fun SecuritySettings(
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun VaultSettings(
|
||||
modifier: Modifier = Modifier,
|
||||
private fun ColumnScope.VaultSettings(
|
||||
defaultSaveOption: DefaultSaveOption,
|
||||
onExportClick: () -> Unit,
|
||||
onImportClick: () -> Unit,
|
||||
@ -298,70 +295,66 @@ private fun VaultSettings(
|
||||
shouldShowDefaultSaveOptions: Boolean,
|
||||
) {
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = BitwardenString.data),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = BitwardenString.import_vault),
|
||||
onClick = onImportClick,
|
||||
modifier = modifier
|
||||
.semantics { testTag = "Import" },
|
||||
withDivider = true,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.testTag("Import"),
|
||||
cardStyle = CardStyle.Top(),
|
||||
content = {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.mirrorIfRtl()
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_navigate_next),
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_chevron_right),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
)
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = BitwardenString.export),
|
||||
onClick = onExportClick,
|
||||
modifier = modifier
|
||||
.semantics { testTag = "Export" },
|
||||
withDivider = true,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.testTag("Export"),
|
||||
cardStyle = CardStyle.Middle(),
|
||||
content = {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.mirrorIfRtl()
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_navigate_next),
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_chevron_right),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
)
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenExternalLinkRow(
|
||||
text = stringResource(BitwardenString.backup),
|
||||
onConfirmClick = onBackupClick,
|
||||
modifier = modifier
|
||||
.semantics { testTag = "Backup" },
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.testTag("Backup"),
|
||||
withDivider = false,
|
||||
dialogTitle = stringResource(BitwardenString.data_backup_title),
|
||||
dialogMessage = stringResource(BitwardenString.data_backup_message),
|
||||
dialogConfirmButtonText = stringResource(BitwardenString.learn_more),
|
||||
dialogDismissButtonText = stringResource(BitwardenString.okay),
|
||||
cardStyle = if (shouldShowSyncWithBitwardenApp || shouldShowDefaultSaveOptions) {
|
||||
CardStyle.Middle()
|
||||
} else {
|
||||
CardStyle.Bottom
|
||||
},
|
||||
)
|
||||
if (shouldShowSyncWithBitwardenApp) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = BitwardenString.sync_with_bitwarden_app),
|
||||
description = annotatedStringResource(
|
||||
id = BitwardenString.learn_more_link,
|
||||
style = spanStyleOf(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
),
|
||||
linkHighlightStyle = spanStyleOf(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textStyle = MaterialTheme.typography.labelLarge,
|
||||
),
|
||||
onAnnotationClick = {
|
||||
when (it) {
|
||||
"learnMore" -> onSyncLearnMoreClick()
|
||||
@ -369,14 +362,18 @@ private fun VaultSettings(
|
||||
},
|
||||
),
|
||||
onClick = onSyncWithBitwardenClick,
|
||||
modifier = modifier,
|
||||
withDivider = true,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
cardStyle = if (shouldShowDefaultSaveOptions) {
|
||||
CardStyle.Middle()
|
||||
} else {
|
||||
CardStyle.Bottom
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
modifier = Modifier.mirrorIfRtl(),
|
||||
painter = painterResource(id = BitwardenDrawable.ic_external_link),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
)
|
||||
},
|
||||
)
|
||||
@ -385,6 +382,7 @@ private fun VaultSettings(
|
||||
DefaultSaveOptionSelectionRow(
|
||||
currentSelection = defaultSaveOption,
|
||||
onSaveOptionUpdated = onDefaultSaveOptionUpdated,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -394,45 +392,22 @@ private fun DefaultSaveOptionSelectionRow(
|
||||
currentSelection: DefaultSaveOption,
|
||||
onSaveOptionUpdated: (DefaultSaveOption) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
resources: Resources = LocalResources.current,
|
||||
) {
|
||||
var shouldShowDefaultSaveOptionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = BitwardenString.default_save_option),
|
||||
onClick = { shouldShowDefaultSaveOptionDialog = true },
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = BitwardenString.default_save_option),
|
||||
dialogSubtitle = stringResource(id = BitwardenString.default_save_options_subtitle),
|
||||
options = DefaultSaveOption.entries.map { it.displayLabel() }.toImmutableList(),
|
||||
selectedOption = currentSelection.displayLabel(),
|
||||
onOptionSelected = { selectedOptionLabel ->
|
||||
val selectedOption = DefaultSaveOption
|
||||
.entries
|
||||
.first { it.displayLabel(resources) == selectedOptionLabel }
|
||||
onSaveOptionUpdated(selectedOption)
|
||||
},
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = modifier,
|
||||
withDivider = true,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
text = currentSelection.displayLabel(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
var dialogSelection by remember { mutableStateOf(currentSelection) }
|
||||
if (shouldShowDefaultSaveOptionDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = BitwardenString.default_save_option),
|
||||
subtitle = stringResource(id = BitwardenString.default_save_options_subtitle),
|
||||
dismissLabel = stringResource(id = BitwardenString.confirm),
|
||||
onDismissRequest = { shouldShowDefaultSaveOptionDialog = false },
|
||||
onDismissActionClick = {
|
||||
onSaveOptionUpdated(dialogSelection)
|
||||
shouldShowDefaultSaveOptionDialog = false
|
||||
},
|
||||
) {
|
||||
DefaultSaveOption.entries.forEach { option ->
|
||||
BitwardenSelectionRow(
|
||||
text = option.displayLabel,
|
||||
isSelected = option == dialogSelection,
|
||||
onClick = {
|
||||
dialogSelection = DefaultSaveOption.entries.first { it == option }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -444,8 +419,9 @@ private fun UnlockWithBiometricsRow(
|
||||
) {
|
||||
if (!biometricsManager.isBiometricsSupported) return
|
||||
var showBiometricsPrompt by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenWideSwitch(
|
||||
BitwardenSwitch(
|
||||
modifier = modifier,
|
||||
cardStyle = CardStyle.Full,
|
||||
label = stringResource(BitwardenString.unlock_with_biometrics),
|
||||
isChecked = isChecked || showBiometricsPrompt,
|
||||
onCheckedChange = { toggled ->
|
||||
@ -472,19 +448,23 @@ private fun UnlockWithBiometricsRow(
|
||||
//region Appearance settings
|
||||
|
||||
@Composable
|
||||
private fun AppearanceSettings(
|
||||
private fun ColumnScope.AppearanceSettings(
|
||||
state: SettingsState,
|
||||
onThemeSelection: (theme: AppTheme) -> Unit,
|
||||
) {
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = BitwardenString.appearance),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
ThemeSelectionRow(
|
||||
currentSelection = state.appearance.theme,
|
||||
onThemeSelection = onThemeSelection,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "ThemeChooser" }
|
||||
.testTag("ThemeChooser")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
@ -494,46 +474,21 @@ private fun ThemeSelectionRow(
|
||||
currentSelection: AppTheme,
|
||||
onThemeSelection: (AppTheme) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
resources: Resources = LocalResources.current,
|
||||
) {
|
||||
var shouldShowThemeSelectionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = BitwardenString.theme),
|
||||
onClick = { shouldShowThemeSelectionDialog = true },
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = BitwardenString.theme),
|
||||
options = AppTheme.entries.map { it.displayLabel() }.toImmutableList(),
|
||||
selectedOption = currentSelection.displayLabel(),
|
||||
onOptionSelected = { selectedOptionLabel ->
|
||||
val selectedOption = AppTheme
|
||||
.entries
|
||||
.first { it.displayLabel(resources) == selectedOptionLabel }
|
||||
onThemeSelection(selectedOption)
|
||||
},
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = modifier,
|
||||
withDivider = true,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.mirrorIfRtl()
|
||||
.size(24.dp),
|
||||
painter = painterResource(
|
||||
id = BitwardenDrawable.ic_navigate_next,
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowThemeSelectionDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = BitwardenString.theme),
|
||||
onDismissRequest = { shouldShowThemeSelectionDialog = false },
|
||||
) {
|
||||
AppTheme.entries.forEach { option ->
|
||||
BitwardenSelectionRow(
|
||||
text = option.displayLabel,
|
||||
isSelected = option == currentSelection,
|
||||
onClick = {
|
||||
shouldShowThemeSelectionDialog = false
|
||||
onThemeSelection(
|
||||
AppTheme.entries.first { it == option },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
//endregion Appearance settings
|
||||
@ -541,32 +496,37 @@ private fun ThemeSelectionRow(
|
||||
//region Help settings
|
||||
|
||||
@Composable
|
||||
private fun HelpSettings(
|
||||
modifier: Modifier = Modifier,
|
||||
private fun ColumnScope.HelpSettings(
|
||||
onTutorialClick: () -> Unit,
|
||||
onHelpCenterClick: () -> Unit,
|
||||
) {
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = BitwardenString.help),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = BitwardenString.launch_tutorial),
|
||||
onClick = onTutorialClick,
|
||||
modifier = modifier
|
||||
.semantics { testTag = "LaunchTutorial" },
|
||||
withDivider = true,
|
||||
modifier = Modifier
|
||||
.testTag("LaunchTutorial")
|
||||
.standardHorizontalMargin(),
|
||||
cardStyle = CardStyle.Top(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenExternalLinkRow(
|
||||
text = stringResource(id = BitwardenString.bitwarden_help_center),
|
||||
onConfirmClick = onHelpCenterClick,
|
||||
modifier = modifier
|
||||
.semantics { testTag = "BitwardenHelpCenter" },
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.testTag("BitwardenHelpCenter"),
|
||||
withDivider = false,
|
||||
dialogTitle = stringResource(id = BitwardenString.continue_to_help_center),
|
||||
dialogMessage = stringResource(
|
||||
BitwardenString.learn_more_about_how_to_use_bitwarden_authenticator_on_the_help_center,
|
||||
),
|
||||
cardStyle = CardStyle.Bottom,
|
||||
)
|
||||
}
|
||||
|
||||
@ -574,38 +534,45 @@ private fun HelpSettings(
|
||||
|
||||
//region About settings
|
||||
@Composable
|
||||
private fun AboutSettings(
|
||||
modifier: Modifier = Modifier,
|
||||
private fun ColumnScope.AboutSettings(
|
||||
state: SettingsState,
|
||||
onSubmitCrashLogsCheckedChange: (Boolean) -> Unit,
|
||||
onPrivacyPolicyClick: () -> Unit,
|
||||
onVersionClick: () -> Unit,
|
||||
) {
|
||||
BitwardenListHeaderText(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = BitwardenString.about),
|
||||
)
|
||||
BitwardenWideSwitch(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.semantics { testTag = "SubmitCrashLogs" },
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenSwitch(
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.testTag("SubmitCrashLogs"),
|
||||
label = stringResource(id = BitwardenString.submit_crash_logs),
|
||||
isChecked = state.isSubmitCrashLogsEnabled,
|
||||
onCheckedChange = onSubmitCrashLogsCheckedChange,
|
||||
cardStyle = CardStyle.Top(),
|
||||
)
|
||||
BitwardenExternalLinkRow(
|
||||
text = stringResource(id = BitwardenString.privacy_policy),
|
||||
modifier = modifier
|
||||
.semantics { testTag = "PrivacyPolicy" },
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.testTag("PrivacyPolicy"),
|
||||
withDivider = false,
|
||||
onConfirmClick = onPrivacyPolicyClick,
|
||||
dialogTitle = stringResource(id = BitwardenString.continue_to_privacy_policy),
|
||||
dialogMessage = stringResource(
|
||||
id = BitwardenString.privacy_policy_description_long,
|
||||
),
|
||||
cardStyle = CardStyle.Middle(),
|
||||
)
|
||||
CopyRow(
|
||||
text = state.version,
|
||||
onClick = onVersionClick,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -617,21 +584,17 @@ private fun CopyRow(
|
||||
resources: Resources = LocalResources.current,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.defaultMinSize(minHeight = 60.dp)
|
||||
.cardStyle(cardStyle = CardStyle.Bottom, onClick = onClick)
|
||||
.semantics(mergeDescendants = true) {
|
||||
contentDescription = text.toString(resources)
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -641,20 +604,15 @@ private fun CopyRow(
|
||||
.padding(end = 16.dp)
|
||||
.weight(1f),
|
||||
text = text(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ic_copy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -663,7 +621,7 @@ private fun CopyRow(
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CopyRow_preview() {
|
||||
AuthenticatorTheme {
|
||||
BitwardenTheme {
|
||||
CopyRow(
|
||||
text = "Copyable Text".asText(),
|
||||
onClick = { },
|
||||
|
||||
@ -7,7 +7,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.padding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@ -22,25 +22,24 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
|
||||
import com.bitwarden.authenticator.ui.platform.util.displayLabel
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
@ -114,10 +113,8 @@ fun ExportScreen(
|
||||
when (val dialog = state.dialogState) {
|
||||
is ExportState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialog.title,
|
||||
message = dialog.message,
|
||||
),
|
||||
title = dialog.title?.invoke(),
|
||||
message = dialog.message(),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ExportAction.DialogDismiss)
|
||||
@ -127,11 +124,7 @@ fun ExportScreen(
|
||||
}
|
||||
|
||||
is ExportState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = dialog.message,
|
||||
),
|
||||
)
|
||||
BitwardenLoadingDialog(text = dialog.message())
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
@ -143,7 +136,7 @@ fun ExportScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.export),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
|
||||
@ -155,11 +148,9 @@ fun ExportScreen(
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
) {
|
||||
ExportScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
onExportFormatOptionSelected = remember(viewModel) {
|
||||
{
|
||||
@ -183,6 +174,7 @@ private fun ExportScreenContent(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = BitwardenString.file_format),
|
||||
options = ExportVaultFormat.entries.map { it.displayLabel() }.toImmutableList(),
|
||||
@ -193,21 +185,25 @@ private fun ExportScreenContent(
|
||||
.first { it.displayLabel(resources) == selectedOptionLabel }
|
||||
onExportFormatOptionSelected(selectedOption)
|
||||
},
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "FileFormatPicker" }
|
||||
.padding(horizontal = 16.dp)
|
||||
.testTag("FileFormatPicker")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
AuthenticatorFilledTonalButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.export),
|
||||
onClick = onExportClick,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "ExportVaultButton" }
|
||||
.padding(horizontal = 16.dp)
|
||||
.testTag("ExportVaultButton")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,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.padding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@ -19,24 +19,24 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.util.displayLabel
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.model.FileData
|
||||
@ -108,11 +108,7 @@ fun ImportingScreen(
|
||||
}
|
||||
|
||||
is ImportState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = dialog.message,
|
||||
),
|
||||
)
|
||||
BitwardenLoadingDialog(text = dialog.message())
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
@ -124,7 +120,7 @@ fun ImportingScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
AuthenticatorTopAppBar(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.import_vault),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
|
||||
@ -136,11 +132,9 @@ fun ImportingScreen(
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
) {
|
||||
ImportScreenContent(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
onImportFormatOptionSelected = remember(viewModel) {
|
||||
{
|
||||
@ -168,6 +162,7 @@ private fun ImportScreenContent(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = BitwardenString.file_format),
|
||||
options = ImportFileFormat.entries.map { it.displayLabel() }.toImmutableList(),
|
||||
@ -178,21 +173,25 @@ private fun ImportScreenContent(
|
||||
.first { it.displayLabel(resources) == selectedOptionLabel }
|
||||
onImportFormatOptionSelected(selectedOption)
|
||||
},
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "FileFormatPicker" }
|
||||
.padding(horizontal = 16.dp)
|
||||
.testTag("FileFormatPicker")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
AuthenticatorFilledTonalButton(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.import_vault),
|
||||
onClick = onImportClick,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "ImportVaultButton" }
|
||||
.padding(horizontal = 16.dp)
|
||||
.testTag("ImportVaultButton")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -41,14 +40,15 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.util.isPortrait
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@ -155,7 +155,7 @@ private fun TutorialScreenContent(
|
||||
.height(44.dp),
|
||||
)
|
||||
|
||||
AuthenticatorFilledTonalButton(
|
||||
BitwardenFilledButton(
|
||||
label = state.actionButtonText,
|
||||
onClick = { continueClick(state.index) },
|
||||
modifier = Modifier
|
||||
@ -163,7 +163,7 @@ private fun TutorialScreenContent(
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
AuthenticatorTextButton(
|
||||
BitwardenTextButton(
|
||||
isEnabled = !state.isLastPage,
|
||||
label = stringResource(id = BitwardenString.skip),
|
||||
onClick = skipClick,
|
||||
@ -196,7 +196,7 @@ private fun TutorialScreenPortrait(
|
||||
Text(
|
||||
text = stringResource(id = state.title),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = BitwardenTheme.typography.headlineMedium,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = 48.dp,
|
||||
@ -206,7 +206,7 @@ private fun TutorialScreenPortrait(
|
||||
Text(
|
||||
text = stringResource(id = state.message),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -235,14 +235,14 @@ private fun TutorialScreenLandscape(
|
||||
Text(
|
||||
text = stringResource(id = state.title),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = BitwardenTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = state.message),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
|
||||
@ -264,7 +264,7 @@ private fun IndicatorDots(
|
||||
) {
|
||||
items(totalCount) { index ->
|
||||
val color = animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.primary.copy(
|
||||
targetValue = BitwardenTheme.colorScheme.icon.secondary.copy(
|
||||
alpha = if (index == selectedIndexProvider()) 1.0f else 0.3f,
|
||||
),
|
||||
label = "dotColor",
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.theme
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bitwarden.authenticator.R
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.util.isDarkMode
|
||||
|
||||
/**
|
||||
* The overall application theme. This can be configured to support a [theme] and [dynamicColor].
|
||||
*/
|
||||
@Composable
|
||||
fun AuthenticatorTheme(
|
||||
theme: AppTheme = AppTheme.DEFAULT,
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val darkTheme = theme.isDarkMode(isSystemDarkMode = isSystemInDarkTheme())
|
||||
// Get the current scheme
|
||||
val context = LocalContext.current
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> darkColorScheme(context)
|
||||
else -> lightColorScheme(context)
|
||||
}
|
||||
|
||||
val nonMaterialColors = if (darkTheme) {
|
||||
darkNonMaterialColors(context)
|
||||
} else {
|
||||
lightNonMaterialColors(context)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalNonMaterialColors provides nonMaterialColors,
|
||||
LocalNonMaterialTypography provides nonMaterialTypography,
|
||||
) {
|
||||
// Set overall theme based on color scheme and typography settings
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun darkColorScheme(context: Context): ColorScheme =
|
||||
androidx.compose.material3.darkColorScheme(
|
||||
primary = R.color.dark_primary.toColor(context),
|
||||
onPrimary = R.color.dark_on_primary.toColor(context),
|
||||
primaryContainer = R.color.dark_primary_container.toColor(context),
|
||||
onPrimaryContainer = R.color.dark_on_primary_container.toColor(context),
|
||||
secondary = R.color.dark_secondary.toColor(context),
|
||||
onSecondary = R.color.dark_on_secondary.toColor(context),
|
||||
secondaryContainer = R.color.dark_secondary_container.toColor(context),
|
||||
onSecondaryContainer = R.color.dark_on_secondary_container.toColor(context),
|
||||
tertiary = R.color.dark_tertiary.toColor(context),
|
||||
onTertiary = R.color.dark_on_tertiary.toColor(context),
|
||||
tertiaryContainer = R.color.dark_tertiary_container.toColor(context),
|
||||
onTertiaryContainer = R.color.dark_on_tertiary_container.toColor(context),
|
||||
error = R.color.dark_error.toColor(context),
|
||||
onError = R.color.dark_on_error.toColor(context),
|
||||
errorContainer = R.color.dark_error_container.toColor(context),
|
||||
onErrorContainer = R.color.dark_on_error_container.toColor(context),
|
||||
surface = R.color.dark_surface.toColor(context),
|
||||
surfaceBright = R.color.dark_surface_bright.toColor(context),
|
||||
surfaceContainer = R.color.dark_surface_container.toColor(context),
|
||||
surfaceContainerHigh = R.color.dark_surface_container_high.toColor(context),
|
||||
surfaceContainerHighest = R.color.dark_surface_container_highest.toColor(context),
|
||||
surfaceContainerLow = R.color.dark_surface_container_low.toColor(context),
|
||||
surfaceContainerLowest = R.color.dark_surface_container_lowest.toColor(context),
|
||||
surfaceVariant = R.color.dark_surface_variant.toColor(context),
|
||||
surfaceDim = R.color.dark_surface_dim.toColor(context),
|
||||
onSurface = R.color.dark_on_surface.toColor(context),
|
||||
onSurfaceVariant = R.color.dark_on_surface_variant.toColor(context),
|
||||
outline = R.color.dark_outline.toColor(context),
|
||||
outlineVariant = R.color.dark_outline_variant.toColor(context),
|
||||
inverseSurface = R.color.dark_inverse_surface.toColor(context),
|
||||
inverseOnSurface = R.color.dark_inverse_on_surface.toColor(context),
|
||||
inversePrimary = R.color.dark_inverse_primary.toColor(context),
|
||||
scrim = R.color.dark_scrim.toColor(context),
|
||||
)
|
||||
|
||||
private fun lightColorScheme(context: Context): ColorScheme =
|
||||
androidx.compose.material3.lightColorScheme(
|
||||
primary = R.color.primary.toColor(context),
|
||||
onPrimary = R.color.on_primary.toColor(context),
|
||||
primaryContainer = R.color.primary_container.toColor(context),
|
||||
onPrimaryContainer = R.color.on_primary_container.toColor(context),
|
||||
secondary = R.color.secondary.toColor(context),
|
||||
onSecondary = R.color.on_secondary.toColor(context),
|
||||
secondaryContainer = R.color.secondary_container.toColor(context),
|
||||
onSecondaryContainer = R.color.on_secondary_container.toColor(context),
|
||||
tertiary = R.color.tertiary.toColor(context),
|
||||
onTertiary = R.color.on_tertiary.toColor(context),
|
||||
tertiaryContainer = R.color.tertiary_container.toColor(context),
|
||||
onTertiaryContainer = R.color.on_tertiary_container.toColor(context),
|
||||
error = R.color.error.toColor(context),
|
||||
onError = R.color.on_error.toColor(context),
|
||||
errorContainer = R.color.error_container.toColor(context),
|
||||
onErrorContainer = R.color.on_error_container.toColor(context),
|
||||
surface = R.color.surface.toColor(context),
|
||||
surfaceBright = R.color.surface_bright.toColor(context),
|
||||
surfaceContainer = R.color.surface_container.toColor(context),
|
||||
surfaceContainerHigh = R.color.surface_container_high.toColor(context),
|
||||
surfaceContainerHighest = R.color.surface_container_highest.toColor(context),
|
||||
surfaceContainerLow = R.color.surface_container_low.toColor(context),
|
||||
surfaceContainerLowest = R.color.surface_container_lowest.toColor(context),
|
||||
surfaceVariant = R.color.surface_variant.toColor(context),
|
||||
surfaceDim = R.color.surface_dim.toColor(context),
|
||||
onSurface = R.color.on_surface.toColor(context),
|
||||
onSurfaceVariant = R.color.on_surface_variant.toColor(context),
|
||||
outline = R.color.outline.toColor(context),
|
||||
outlineVariant = R.color.outline_variant.toColor(context),
|
||||
inverseSurface = R.color.inverse_surface.toColor(context),
|
||||
inverseOnSurface = R.color.inverse_on_surface.toColor(context),
|
||||
inversePrimary = R.color.inverse_primary.toColor(context),
|
||||
scrim = R.color.scrim.toColor(context),
|
||||
)
|
||||
|
||||
@ColorRes
|
||||
private fun Int.toColor(context: Context): Color =
|
||||
Color(context.getColor(this))
|
||||
|
||||
/**
|
||||
* Provides access to non material theme typography throughout the app.
|
||||
*/
|
||||
val LocalNonMaterialTypography: ProvidableCompositionLocal<NonMaterialTypography> =
|
||||
compositionLocalOf { nonMaterialTypography }
|
||||
|
||||
/**
|
||||
* Provides access to non material theme colors throughout the app.
|
||||
*/
|
||||
val LocalNonMaterialColors: ProvidableCompositionLocal<NonMaterialColors> =
|
||||
compositionLocalOf {
|
||||
// Default value here will immediately be overridden in BitwardenTheme, similar
|
||||
// to how MaterialTheme works.
|
||||
NonMaterialColors(
|
||||
fingerprint = Color.Transparent,
|
||||
qrCodeClickableText = Color.Transparent,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Models colors that live outside of the Material Theme spec.
|
||||
*/
|
||||
data class NonMaterialColors(
|
||||
val fingerprint: Color,
|
||||
val qrCodeClickableText: Color,
|
||||
)
|
||||
|
||||
private fun lightNonMaterialColors(context: Context): NonMaterialColors =
|
||||
NonMaterialColors(
|
||||
fingerprint = R.color.light_fingerprint.toColor(context),
|
||||
qrCodeClickableText = R.color.qr_code_clickable_text.toColor(context),
|
||||
)
|
||||
|
||||
private fun darkNonMaterialColors(context: Context): NonMaterialColors =
|
||||
NonMaterialColors(
|
||||
fingerprint = R.color.dark_fingerprint.toColor(context),
|
||||
qrCodeClickableText = R.color.qr_code_clickable_text.toColor(context),
|
||||
)
|
||||
@ -1,255 +0,0 @@
|
||||
package com.bitwarden.authenticator.ui.platform.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.PlatformTextStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitwarden.authenticator.R
|
||||
|
||||
val Typography: Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = (-0.25).sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = (0).sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_medium)),
|
||||
fontWeight = FontWeight.W500,
|
||||
letterSpacing = 0.15.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_medium)),
|
||||
fontWeight = FontWeight.W500,
|
||||
letterSpacing = 0.1.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.25.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.4.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_medium)),
|
||||
fontWeight = FontWeight.W500,
|
||||
letterSpacing = 0.1.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_medium)),
|
||||
fontWeight = FontWeight.W500,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_medium)),
|
||||
fontWeight = FontWeight.W500,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
)
|
||||
|
||||
val nonMaterialTypography: NonMaterialTypography = NonMaterialTypography(
|
||||
sensitiveInfoSmall = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular_mono)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
sensitiveInfoMedium = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular_mono)),
|
||||
fontWeight = FontWeight.W400,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
bodySmallProminent = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W700,
|
||||
letterSpacing = 0.4.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
labelMediumProminent = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.roboto_regular)),
|
||||
fontWeight = FontWeight.W600,
|
||||
letterSpacing = 0.5.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Models typography that live outside of the Material Theme spec.
|
||||
*/
|
||||
data class NonMaterialTypography(
|
||||
val bodySmallProminent: TextStyle,
|
||||
val labelMediumProminent: TextStyle,
|
||||
val sensitiveInfoSmall: TextStyle,
|
||||
val sensitiveInfoMedium: TextStyle,
|
||||
)
|
||||
@ -11,7 +11,6 @@ import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class MainViewModelTest : BaseViewModelTest() {
|
||||
@ -24,35 +23,28 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow
|
||||
}
|
||||
private val fakeServerConfigRepository = FakeServerConfigRepository()
|
||||
private lateinit var mainViewModel: MainViewModel
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mainViewModel = MainViewModel(
|
||||
settingsRepository,
|
||||
fakeServerConfigRepository,
|
||||
)
|
||||
}
|
||||
private val mainViewModel: MainViewModel = MainViewModel(
|
||||
settingsRepository = settingsRepository,
|
||||
configRepository = fakeServerConfigRepository,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `on AppThemeChanged should update state`() {
|
||||
assertEquals(
|
||||
MainState(
|
||||
theme = AppTheme.DEFAULT,
|
||||
),
|
||||
mainViewModel.stateFlow.value,
|
||||
)
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.Internal.ThemeUpdate(
|
||||
theme = AppTheme.DARK,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
MainState(
|
||||
theme = AppTheme.DARK,
|
||||
),
|
||||
mainViewModel.stateFlow.value,
|
||||
)
|
||||
fun `on AppThemeChanged should update state`() = runTest {
|
||||
mainViewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
assertEquals(
|
||||
MainState(theme = AppTheme.DEFAULT),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
mainViewModel.trySendAction(MainAction.Internal.ThemeUpdate(theme = AppTheme.DARK))
|
||||
assertEquals(
|
||||
MainState(theme = AppTheme.DARK),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
MainEvent.UpdateAppTheme(osTheme = AppTheme.DARK.osValue),
|
||||
eventFlow.expectMostRecentItem(),
|
||||
)
|
||||
}
|
||||
|
||||
verify {
|
||||
settingsRepository.appTheme
|
||||
@ -62,10 +54,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `send NavigateToDebugMenu action when OpenDebugMenu action is sent`() = runTest {
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
|
||||
mainViewModel.eventFlow.test {
|
||||
awaitItem() // ignore first event
|
||||
// Ignore the events that are fired off by flows in the ViewModel init
|
||||
skipItems(2)
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
assertEquals(MainEvent.NavigateToDebugMenu, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@ import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onSiblings
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
|
||||
@ -152,12 +152,12 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
|
||||
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_CONTENT) }
|
||||
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Information").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "INFORMATION").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Name").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Key").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Username").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Favorite").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Advanced").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll(text = "Additional options").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -211,7 +211,7 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
|
||||
fun `advanced click should send ExpandAdvancedOptionsClick`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_CONTENT) }
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Advanced")
|
||||
.onNodeWithTextAfterScroll(text = "Additional options")
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(EditItemAction.ExpandAdvancedOptionsClick)
|
||||
@ -356,7 +356,7 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Number of digits")
|
||||
.onSiblings()
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("+"))
|
||||
.performClick()
|
||||
|
||||
@ -372,7 +372,7 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Number of digits")
|
||||
.onSiblings()
|
||||
.onChildren()
|
||||
.filterToOne(hasContentDescription("\u2212"))
|
||||
.performClick()
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.bitwarden.ui.util.performCustomAccessibilityAction
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
@ -194,7 +193,7 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on permission dialog Settings clock should emit SettingsClick`() {
|
||||
fun `on permission dialog Settings click should emit SettingsClick`() {
|
||||
permissionsManager.checkPermissionResult = false
|
||||
permissionsManager.getPermissionsResult = false
|
||||
composeTestRule
|
||||
@ -227,21 +226,6 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on Scan QR code accessibility action without permission and permission is granted should emit ScanQrCodeTextClick`() {
|
||||
permissionsManager.checkPermissionResult = false
|
||||
permissionsManager.getPermissionsResult = true
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Scan QR code")
|
||||
.performScrollTo()
|
||||
.performCustomAccessibilityAction(label = "Scan QR code")
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on dialog should updates according to state`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
@ -5,10 +5,10 @@ import com.bitwarden.authenticator.ui.platform.composition.LocalManagerProvider
|
||||
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.bitwarden.authenticator.ui.platform.manager.exit.ExitManager
|
||||
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
import com.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import io.mockk.mockk
|
||||
|
||||
/**
|
||||
@ -19,7 +19,7 @@ abstract class AuthenticatorComposeTest : BaseComposeTest() {
|
||||
|
||||
/**
|
||||
* Helper for testing a basic Composable function that only requires a Composable environment
|
||||
* with the [AuthenticatorTheme].
|
||||
* with the [BitwardenTheme].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
protected fun setContent(
|
||||
@ -31,7 +31,7 @@ abstract class AuthenticatorComposeTest : BaseComposeTest() {
|
||||
test: @Composable () -> Unit,
|
||||
) {
|
||||
setTestContent {
|
||||
AuthenticatorTheme(theme = theme) {
|
||||
BitwardenTheme(theme = theme) {
|
||||
LocalManagerProvider(
|
||||
permissionsManager = permissionsManager,
|
||||
intentManager = intentManager,
|
||||
|
||||
@ -143,7 +143,7 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `Default Save Option dialog should send DefaultSaveOptionUpdated when confirm is clicked`() =
|
||||
fun `Default Save Option dialog should send DefaultSaveOptionUpdated when selection is made`() =
|
||||
runTest {
|
||||
val expectedSaveOption = DefaultSaveOption.BITWARDEN_APP
|
||||
mutableStateFlow.value = DEFAULT_STATE
|
||||
@ -164,12 +164,6 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
// Click confirm:
|
||||
composeTestRule
|
||||
.onNodeWithText("Confirm")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SettingsAction.DataClick.DefaultSaveOptionUpdated(expectedSaveOption),
|
||||
|
||||
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -20,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitwarden.ui.R
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The default [BitwardenTypography] for the app.
|
||||
@ -279,7 +279,7 @@ fun BitwardenTypography.toMaterialTypography(): Typography = Typography(
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenTypography_preview() {
|
||||
MaterialTheme {
|
||||
BitwardenTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
|
||||
18
ui/src/main/res/drawable-night/ic_verification_codes.xml
Normal file
18
ui/src/main/res/drawable-night/ic_verification_codes.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:fillColor="#96A3BB"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M16.96,9.146C17.349,9.538 17.346,10.171 16.954,10.56L10.902,16.56C10.512,16.946 9.884,16.946 9.494,16.56L6.546,13.637C6.154,13.248 6.151,12.615 6.54,12.222C6.929,11.83 7.562,11.828 7.954,12.216L10.198,14.441L15.546,9.139C15.938,8.751 16.571,8.753 16.96,9.146Z" />
|
||||
<path
|
||||
android:fillColor="#96A3BB"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.001,0.851C11.448,0.851 11.001,1.298 11.001,1.851C11.001,2.403 11.448,2.851 12.001,2.851C17.524,2.851 22.001,7.328 22.001,12.851C22.001,18.373 17.524,22.851 12.001,22.851C7.666,22.851 3.974,20.093 2.587,16.233C2.4,15.713 1.827,15.443 1.308,15.63C0.788,15.817 0.518,16.389 0.705,16.909C2.368,21.538 6.796,24.851 12.001,24.851C18.628,24.851 24.001,19.478 24.001,12.851C24.001,6.223 18.628,0.851 12.001,0.851Z" />
|
||||
<path
|
||||
android:fillColor="#96A3BB"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.001,2.851C11.836,2.851 11.671,2.854 11.508,2.862C10.957,2.889 10.488,2.463 10.461,1.912C10.434,1.36 10.86,0.891 11.412,0.865C11.607,0.855 11.804,0.851 12.001,0.851C12.553,0.851 13,1.298 13,1.85C13,2.403 12.553,2.851 12.001,2.851ZM9.235,2.154C9.421,2.674 9.15,3.246 8.63,3.432C8.321,3.543 8.018,3.668 7.724,3.808C7.225,4.044 6.629,3.831 6.392,3.332C6.156,2.833 6.369,2.237 6.868,2C7.221,1.833 7.584,1.682 7.956,1.549C8.476,1.363 9.049,1.634 9.235,2.154ZM5.354,4.028C5.725,4.437 5.694,5.07 5.285,5.441C5.042,5.661 4.81,5.892 4.59,6.135C4.219,6.544 3.587,6.575 3.178,6.204C2.769,5.833 2.738,5.201 3.109,4.791C3.372,4.501 3.65,4.223 3.941,3.959C4.35,3.588 4.983,3.619 5.354,4.028ZM2.482,7.243C2.981,7.479 3.194,8.075 2.957,8.574C2.818,8.869 2.692,9.171 2.582,9.48C2.396,10 1.823,10.271 1.303,10.085C0.783,9.899 0.513,9.327 0.699,8.807C0.832,8.435 0.983,8.072 1.15,7.718C1.387,7.219 1.983,7.006 2.482,7.243ZM1.061,11.312C1.613,11.338 2.039,11.807 2.012,12.359C2.004,12.522 2,12.686 2,12.85C2,13.056 2.006,13.26 2.018,13.462C2.052,14.013 1.632,14.487 1.08,14.52C0.529,14.554 0.055,14.134 0.022,13.582C0.007,13.34 0,13.096 0,12.85C0,12.653 0.005,12.457 0.014,12.262C0.041,11.71 0.51,11.285 1.061,11.312ZM1.573,16.503C2.075,16.274 2.668,16.496 2.898,16.998C2.982,17.184 3.073,17.366 3.168,17.545C3.428,18.033 3.243,18.638 2.756,18.898C2.268,19.158 1.663,18.973 1.403,18.486C1.288,18.271 1.18,18.051 1.078,17.828C0.849,17.326 1.07,16.733 1.573,16.503Z" />
|
||||
</vector>
|
||||
13
ui/src/main/res/drawable/ic_lock_encrypted_small.xml
Normal file
13
ui/src/main/res/drawable/ic_lock_encrypted_small.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="17dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="17">
|
||||
<path
|
||||
android:pathData="M4,4.333C4,2.124 5.791,0.333 8,0.333C10.209,0.333 12,2.124 12,4.333V5.333H13C14.105,5.333 15,6.228 15,7.333C15,7.747 14.664,8.083 14.25,8.083C13.836,8.083 13.5,7.747 13.5,7.333C13.5,7.057 13.276,6.833 13,6.833H3C2.724,6.833 2.5,7.057 2.5,7.333V14.333C2.5,14.609 2.724,14.833 3,14.833H7.25C7.664,14.833 8,15.169 8,15.583C8,15.997 7.664,16.333 7.25,16.333H3C1.895,16.333 1,15.438 1,14.333V7.333C1,6.228 1.895,5.333 3,5.333H4V4.333ZM10.5,4.333V5.333H5.5V4.333C5.5,2.952 6.619,1.833 8,1.833C9.381,1.833 10.5,2.952 10.5,4.333Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M12.754,9.547C12.754,9.133 12.418,8.797 12.004,8.797C11.59,8.797 11.254,9.133 11.254,9.547V11.687L9.242,11.027C8.848,10.899 8.424,11.113 8.295,11.507C8.167,11.9 8.381,12.324 8.775,12.453L10.801,13.116L9.54,14.883C9.299,15.22 9.377,15.688 9.714,15.929C10.051,16.17 10.52,16.091 10.76,15.754L12.004,14.012L13.248,15.755C13.489,16.091 13.958,16.17 14.295,15.929C14.632,15.688 14.71,15.22 14.469,14.883L13.208,13.116L15.233,12.453C15.627,12.324 15.842,11.9 15.713,11.507C15.584,11.113 15.16,10.899 14.767,11.027L12.754,11.687V9.547Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
@ -1,13 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<group>
|
||||
<clip-path android:pathData="M0,0h24v24h-24z" />
|
||||
<path
|
||||
android:fillColor="#001550"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M6.355,5.259C10.078,1.536 16.114,1.536 19.837,5.259C23.559,8.982 23.559,15.018 19.837,18.741C17.65,20.927 14.666,21.83 11.819,21.448C11.444,21.397 11.099,21.661 11.048,22.036C10.998,22.411 11.261,22.756 11.636,22.807C14.889,23.244 18.305,22.212 20.806,19.711C25.065,15.452 25.065,8.548 20.806,4.29C16.548,0.031 9.644,0.031 5.385,4.29C4.14,5.535 3.258,7.009 2.741,8.577L1.615,6.576C1.43,6.246 1.012,6.129 0.682,6.315C0.352,6.5 0.235,6.918 0.42,7.248L2.355,10.69C2.541,11.02 2.959,11.137 3.289,10.951L6.73,9.016C7.06,8.83 7.177,8.412 6.992,8.082C6.806,7.752 6.388,7.635 6.058,7.821L4.064,8.942C4.519,7.597 5.282,6.333 6.355,5.259ZM15.372,6.408C16.315,6.408 17.084,7.178 17.084,8.121V9.394H13.659V8.121C13.659,7.174 14.428,6.408 15.372,6.408ZM12.836,9.394V8.121C12.836,6.718 13.975,5.586 15.372,5.586C16.769,5.586 17.907,6.724 17.907,8.121V9.394H18.324C18.702,9.394 19.01,9.701 19.01,10.08V15.016C19.01,15.395 18.702,15.701 18.324,15.701H15.834V18.421C15.834,18.58 15.794,18.744 15.705,18.881C15.618,19.014 15.451,19.166 15.207,19.166H0.627C0.388,19.166 0.219,19.019 0.129,18.885C0.038,18.748 -0.005,18.581 0,18.414V13.314C0,13.155 0.041,12.991 0.129,12.854C0.216,12.72 0.383,12.569 0.627,12.569H11.836V10.08C11.836,9.701 12.143,9.394 12.521,9.394H12.836ZM12.658,12.569H15.207C15.451,12.569 15.618,12.72 15.705,12.854C15.794,12.991 15.834,13.155 15.834,13.314V14.879H18.187V10.217H12.658V12.569ZM15.011,14.879V13.392H0.823V18.343H15.011V15.701H14.993V14.879H15.011ZM8.065,16.623C8.097,16.456 8.113,16.258 8.113,16.027C8.113,15.601 8.052,15.272 7.93,15.039C7.86,14.905 7.773,14.793 7.67,14.703C7.569,14.611 7.452,14.542 7.317,14.496C7.182,14.448 7.032,14.424 6.867,14.424C6.617,14.424 6.402,14.478 6.22,14.585C6.041,14.69 5.904,14.845 5.809,15.048C5.755,15.17 5.715,15.317 5.689,15.487C5.663,15.658 5.65,15.85 5.65,16.063C5.65,16.227 5.662,16.381 5.684,16.523C5.708,16.664 5.745,16.793 5.795,16.91C5.893,17.125 6.038,17.292 6.23,17.413C6.424,17.535 6.643,17.595 6.887,17.595C7.098,17.595 7.291,17.55 7.464,17.46C7.637,17.37 7.778,17.242 7.887,17.075C7.973,16.94 8.033,16.789 8.065,16.623ZM7.401,15.352C7.436,15.516 7.454,15.726 7.454,15.984C7.454,16.256 7.437,16.476 7.404,16.644C7.37,16.812 7.311,16.941 7.228,17.031C7.145,17.121 7.029,17.166 6.882,17.166C6.739,17.166 6.626,17.123 6.543,17.037C6.459,16.95 6.4,16.822 6.365,16.652C6.329,16.483 6.312,16.266 6.312,16.001C6.312,15.611 6.353,15.322 6.434,15.134C6.518,14.947 6.665,14.853 6.877,14.853C7.024,14.853 7.14,14.896 7.223,14.98C7.307,15.063 7.366,15.187 7.401,15.352ZM3.223,15.333V17.257C3.223,17.37 3.253,17.455 3.312,17.513C3.371,17.571 3.45,17.6 3.548,17.6C3.767,17.6 3.877,17.458 3.877,17.174V14.754C3.877,14.652 3.852,14.573 3.8,14.515C3.749,14.457 3.681,14.428 3.596,14.428C3.52,14.428 3.469,14.441 3.442,14.466C3.415,14.492 3.356,14.559 3.266,14.669C3.177,14.779 3.073,14.878 2.956,14.967C2.841,15.056 2.686,15.141 2.492,15.221C2.362,15.275 2.272,15.318 2.22,15.352C2.169,15.386 2.143,15.439 2.143,15.511C2.143,15.573 2.169,15.628 2.22,15.676C2.273,15.722 2.333,15.745 2.401,15.745C2.542,15.745 2.816,15.608 3.223,15.333ZM10.396,17.257V15.333C9.989,15.608 9.715,15.745 9.574,15.745C9.506,15.745 9.446,15.722 9.393,15.676C9.342,15.628 9.316,15.573 9.316,15.511C9.316,15.439 9.342,15.386 9.393,15.352C9.445,15.318 9.535,15.275 9.665,15.221C9.859,15.141 10.014,15.056 10.129,14.967C10.246,14.878 10.35,14.779 10.439,14.669C10.529,14.559 10.588,14.492 10.615,14.466C10.642,14.441 10.693,14.428 10.769,14.428C10.854,14.428 10.922,14.457 10.973,14.515C11.025,14.573 11.05,14.652 11.05,14.754V17.174C11.05,17.458 10.94,17.6 10.721,17.6C10.623,17.6 10.544,17.571 10.485,17.513C10.426,17.455 10.396,17.37 10.396,17.257ZM13.325,15.333V17.257C13.325,17.37 13.355,17.455 13.414,17.513C13.473,17.571 13.552,17.6 13.65,17.6C13.869,17.6 13.979,17.458 13.979,17.174V14.754C13.979,14.652 13.954,14.573 13.902,14.515C13.851,14.457 13.783,14.428 13.698,14.428C13.623,14.428 13.571,14.441 13.544,14.466C13.517,14.492 13.458,14.559 13.368,14.669C13.279,14.779 13.175,14.878 13.058,14.967C12.943,15.056 12.788,15.141 12.594,15.221C12.464,15.275 12.374,15.318 12.322,15.352C12.271,15.386 12.245,15.439 12.245,15.511C12.245,15.573 12.271,15.628 12.322,15.676C12.375,15.722 12.435,15.745 12.503,15.745C12.644,15.745 12.918,15.608 13.325,15.333Z" />
|
||||
</group>
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:fillColor="#5A6D91"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M16.96,9.146C17.349,9.538 17.346,10.171 16.954,10.56L10.902,16.56C10.512,16.946 9.884,16.946 9.494,16.56L6.546,13.637C6.154,13.248 6.151,12.615 6.54,12.222C6.929,11.83 7.562,11.828 7.954,12.216L10.198,14.441L15.546,9.139C15.938,8.751 16.571,8.753 16.96,9.146Z" />
|
||||
<path
|
||||
android:fillColor="#5A6D91"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.001,0.851C11.448,0.851 11.001,1.298 11.001,1.851C11.001,2.403 11.448,2.851 12.001,2.851C17.524,2.851 22.001,7.328 22.001,12.851C22.001,18.373 17.524,22.851 12.001,22.851C7.666,22.851 3.974,20.093 2.587,16.233C2.4,15.713 1.827,15.443 1.308,15.63C0.788,15.817 0.518,16.389 0.705,16.909C2.368,21.538 6.796,24.851 12.001,24.851C18.628,24.851 24.001,19.478 24.001,12.851C24.001,6.223 18.628,0.851 12.001,0.851Z" />
|
||||
<path
|
||||
android:fillColor="#5A6D91"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.001,2.851C11.836,2.851 11.671,2.854 11.508,2.862C10.957,2.889 10.488,2.463 10.461,1.912C10.434,1.36 10.86,0.891 11.412,0.865C11.607,0.855 11.804,0.851 12.001,0.851C12.553,0.851 13,1.298 13,1.85C13,2.403 12.553,2.851 12.001,2.851ZM9.235,2.154C9.421,2.674 9.15,3.246 8.63,3.432C8.321,3.543 8.018,3.668 7.724,3.808C7.225,4.044 6.629,3.831 6.392,3.332C6.156,2.833 6.369,2.237 6.868,2C7.221,1.833 7.584,1.682 7.956,1.549C8.476,1.363 9.049,1.634 9.235,2.154ZM5.354,4.028C5.725,4.437 5.694,5.07 5.285,5.441C5.042,5.661 4.81,5.892 4.59,6.135C4.219,6.544 3.587,6.575 3.178,6.204C2.769,5.833 2.738,5.201 3.109,4.791C3.372,4.501 3.65,4.223 3.941,3.959C4.35,3.588 4.983,3.619 5.354,4.028ZM2.482,7.243C2.981,7.479 3.194,8.075 2.957,8.574C2.818,8.869 2.692,9.171 2.582,9.48C2.396,10 1.823,10.271 1.303,10.085C0.783,9.899 0.513,9.327 0.699,8.807C0.832,8.435 0.983,8.072 1.15,7.718C1.387,7.219 1.983,7.006 2.482,7.243ZM1.061,11.312C1.613,11.338 2.039,11.807 2.012,12.359C2.004,12.522 2,12.686 2,12.85C2,13.056 2.006,13.26 2.018,13.462C2.052,14.013 1.632,14.487 1.08,14.52C0.529,14.554 0.055,14.134 0.022,13.582C0.007,13.34 0,13.096 0,12.85C0,12.653 0.005,12.457 0.014,12.262C0.041,11.71 0.51,11.285 1.061,11.312ZM1.573,16.503C2.075,16.274 2.668,16.496 2.898,16.998C2.982,17.184 3.073,17.366 3.168,17.545C3.428,18.033 3.243,18.638 2.756,18.898C2.268,19.158 1.663,18.973 1.403,18.486C1.288,18.271 1.18,18.051 1.078,17.828C0.849,17.326 1.07,16.733 1.573,16.503Z" />
|
||||
</vector>
|
||||
|
||||
@ -1,14 +1,29 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M6.355,5.259C10.078,1.536 16.114,1.536 19.837,5.259C23.559,8.982 23.559,15.018 19.837,18.741C17.65,20.927 14.666,21.83 11.819,21.448C11.444,21.397 11.099,21.661 11.048,22.036C10.998,22.411 11.261,22.756 11.636,22.807C14.889,23.244 18.305,22.212 20.806,19.711C25.065,15.452 25.065,8.548 20.806,4.29C16.548,0.031 9.644,0.031 5.385,4.29C4.14,5.535 3.258,7.009 2.741,8.577L1.615,6.576C1.43,6.246 1.012,6.129 0.682,6.315C0.352,6.5 0.235,6.918 0.42,7.248L2.355,10.69C2.541,11.02 2.959,11.137 3.289,10.951L6.73,9.016C7.06,8.83 7.177,8.412 6.992,8.082C6.806,7.752 6.388,7.635 6.058,7.821L4.064,8.942C4.519,7.597 5.282,6.333 6.355,5.259ZM15.372,6.408C16.315,6.408 17.084,7.178 17.084,8.121V9.394H13.659V8.121C13.659,7.174 14.428,6.408 15.372,6.408ZM12.836,9.394V8.121C12.836,6.718 13.975,5.586 15.372,5.586C16.769,5.586 17.907,6.724 17.907,8.121V9.394H18.324C18.702,9.394 19.01,9.701 19.01,10.08V15.016C19.01,15.395 18.702,15.701 18.324,15.701H15.834V18.421C15.834,18.58 15.794,18.744 15.705,18.881C15.618,19.014 15.451,19.166 15.207,19.166H0.627C0.388,19.166 0.219,19.019 0.129,18.885C0.038,18.748 -0.005,18.581 0,18.414V13.314C0,13.155 0.041,12.991 0.129,12.854C0.216,12.72 0.383,12.569 0.627,12.569H11.836V10.08C11.836,9.701 12.143,9.394 12.521,9.394H12.836ZM12.658,12.569H15.207C15.451,12.569 15.618,12.72 15.705,12.854C15.794,12.991 15.834,13.155 15.834,13.314V14.879H18.187V10.217H12.658V12.569ZM15.011,14.879V13.392H0.823V18.343H15.011V15.701H14.993V14.879H15.011ZM8.065,16.623C8.097,16.456 8.113,16.258 8.113,16.027C8.113,15.601 8.052,15.272 7.93,15.039C7.86,14.905 7.773,14.793 7.67,14.703C7.569,14.611 7.452,14.542 7.317,14.496C7.182,14.448 7.032,14.424 6.867,14.424C6.617,14.424 6.402,14.478 6.22,14.585C6.041,14.69 5.904,14.845 5.809,15.048C5.755,15.17 5.715,15.317 5.689,15.487C5.663,15.658 5.65,15.85 5.65,16.063C5.65,16.227 5.662,16.381 5.684,16.523C5.708,16.664 5.745,16.793 5.795,16.91C5.893,17.125 6.038,17.292 6.23,17.413C6.424,17.535 6.643,17.595 6.887,17.595C7.098,17.595 7.291,17.55 7.464,17.46C7.637,17.37 7.778,17.242 7.887,17.075C7.973,16.94 8.033,16.789 8.065,16.623ZM7.401,15.352C7.436,15.516 7.454,15.726 7.454,15.984C7.454,16.256 7.437,16.476 7.404,16.644C7.37,16.812 7.311,16.941 7.228,17.031C7.145,17.121 7.029,17.166 6.882,17.166C6.739,17.166 6.626,17.123 6.543,17.037C6.459,16.95 6.4,16.822 6.365,16.652C6.329,16.483 6.312,16.266 6.312,16.001C6.312,15.611 6.353,15.322 6.434,15.134C6.518,14.947 6.665,14.853 6.877,14.853C7.024,14.853 7.14,14.896 7.223,14.98C7.307,15.063 7.366,15.187 7.401,15.352ZM3.223,15.333V17.257C3.223,17.37 3.253,17.455 3.312,17.513C3.371,17.571 3.45,17.6 3.548,17.6C3.767,17.6 3.877,17.458 3.877,17.174V14.754C3.877,14.652 3.852,14.573 3.8,14.515C3.749,14.457 3.681,14.428 3.596,14.428C3.52,14.428 3.469,14.441 3.442,14.466C3.415,14.492 3.356,14.559 3.266,14.669C3.177,14.779 3.073,14.878 2.956,14.967C2.841,15.056 2.686,15.141 2.492,15.221C2.362,15.275 2.272,15.318 2.22,15.352C2.169,15.386 2.143,15.439 2.143,15.511C2.143,15.573 2.169,15.628 2.22,15.676C2.273,15.722 2.333,15.745 2.401,15.745C2.542,15.745 2.816,15.608 3.223,15.333ZM10.396,17.257V15.333C9.989,15.608 9.715,15.745 9.574,15.745C9.506,15.745 9.446,15.722 9.393,15.676C9.342,15.628 9.316,15.573 9.316,15.511C9.316,15.439 9.342,15.386 9.393,15.352C9.445,15.318 9.535,15.275 9.665,15.221C9.859,15.141 10.014,15.056 10.129,14.967C10.246,14.878 10.35,14.779 10.439,14.669C10.529,14.559 10.588,14.492 10.615,14.466C10.642,14.441 10.693,14.428 10.769,14.428C10.854,14.428 10.922,14.457 10.973,14.515C11.025,14.573 11.05,14.652 11.05,14.754V17.174C11.05,17.458 10.94,17.6 10.721,17.6C10.623,17.6 10.544,17.571 10.485,17.513C10.426,17.455 10.396,17.37 10.396,17.257ZM13.325,15.333V17.257C13.325,17.37 13.355,17.455 13.414,17.513C13.473,17.571 13.552,17.6 13.65,17.6C13.869,17.6 13.979,17.458 13.979,17.174V14.754C13.979,14.652 13.954,14.573 13.902,14.515C13.851,14.457 13.783,14.428 13.698,14.428C13.623,14.428 13.571,14.441 13.544,14.466C13.517,14.492 13.458,14.559 13.368,14.669C13.279,14.779 13.175,14.878 13.058,14.967C12.943,15.056 12.788,15.141 12.594,15.221C12.464,15.275 12.374,15.318 12.322,15.352C12.271,15.386 12.245,15.439 12.245,15.511C12.245,15.573 12.271,15.628 12.322,15.676C12.375,15.722 12.435,15.745 12.503,15.745C12.644,15.745 12.918,15.608 13.325,15.333Z"
|
||||
android:fillColor="#001550"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
android:name="navigationActiveAccent"
|
||||
android:fillColor="#DBE5F6"
|
||||
android:pathData="M21,12C21,16.971 16.971,21 12,21C7.029,21 3,16.971 3,12C3,7.029 7.029,3 12,3C16.971,3 21,7.029 21,12Z" />
|
||||
<path
|
||||
android:name="navigationActiveAccent"
|
||||
android:fillColor="#DBE5F6"
|
||||
android:pathData="M20,12.25C20,16.668 16.418,20.25 12,20.25C7.582,20.25 4,16.668 4,12.25C4,7.832 7.582,4.25 12,4.25C16.418,4.25 20,7.832 20,12.25Z" />
|
||||
<path
|
||||
android:name="navigation"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M16.96,8.546C17.349,8.938 17.346,9.571 16.954,9.96L10.902,15.96C10.512,16.347 9.884,16.347 9.494,15.96L6.546,13.037C6.154,12.648 6.151,12.015 6.54,11.623C6.929,11.231 7.562,11.228 7.954,11.617L10.198,13.842L15.546,8.54C15.938,8.151 16.571,8.154 16.96,8.546Z" />
|
||||
<path
|
||||
android:name="navigation"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.001,0.25C11.448,0.25 11.001,0.698 11.001,1.25C11.001,1.802 11.448,2.25 12.001,2.25C17.524,2.25 22.001,6.727 22.001,12.25C22.001,17.773 17.524,22.25 12.001,22.25C7.666,22.25 3.974,19.492 2.587,15.632C2.4,15.112 1.827,14.842 1.308,15.029C0.788,15.216 0.518,15.789 0.705,16.309C2.368,20.937 6.796,24.25 12.001,24.25C18.628,24.25 24.001,18.877 24.001,12.25C24.001,5.623 18.628,0.25 12.001,0.25Z" />
|
||||
<path
|
||||
android:name="navigation"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.001,2.25C11.836,2.25 11.671,2.254 11.508,2.262C10.957,2.288 10.488,1.863 10.461,1.311C10.434,0.759 10.86,0.291 11.412,0.264C11.607,0.255 11.804,0.25 12.001,0.25C12.553,0.25 13,0.698 13,1.25C13,1.802 12.553,2.25 12.001,2.25ZM9.235,1.553C9.421,2.073 9.15,2.646 8.63,2.832C8.321,2.942 8.018,3.068 7.724,3.207C7.225,3.444 6.629,3.231 6.392,2.732C6.156,2.233 6.369,1.636 6.868,1.4C7.221,1.232 7.584,1.082 7.956,0.949C8.476,0.762 9.049,1.033 9.235,1.553ZM5.354,3.427C5.725,3.837 5.694,4.469 5.285,4.84C5.042,5.06 4.81,5.292 4.59,5.534C4.219,5.944 3.587,5.974 3.178,5.603C2.769,5.232 2.738,4.6 3.109,4.191C3.372,3.9 3.65,3.622 3.941,3.359C4.35,2.987 4.983,3.018 5.354,3.427ZM2.482,6.642C2.981,6.878 3.194,7.475 2.957,7.974C2.818,8.268 2.692,8.571 2.582,8.88C2.396,9.4 1.823,9.67 1.303,9.484C0.783,9.298 0.513,8.726 0.699,8.206C0.832,7.834 0.983,7.471 1.15,7.117C1.387,6.618 1.983,6.405 2.482,6.642ZM1.061,10.711C1.613,10.738 2.039,11.207 2.012,11.758C2.004,11.921 2,12.085 2,12.25C2,12.455 2.006,12.659 2.018,12.861C2.052,13.413 1.632,13.887 1.08,13.92C0.529,13.953 0.055,13.533 0.022,12.982C0.007,12.74 0,12.495 0,12.25C0,12.053 0.005,11.857 0.014,11.662C0.041,11.11 0.51,10.684 1.061,10.711ZM1.573,15.903C2.075,15.674 2.668,15.895 2.898,16.397C2.982,16.583 3.073,16.765 3.168,16.945C3.428,17.432 3.243,18.038 2.756,18.298C2.268,18.557 1.663,18.373 1.403,17.885C1.288,17.67 1.18,17.451 1.078,17.228C0.849,16.725 1.07,16.132 1.573,15.903Z" />
|
||||
</vector>
|
||||
|
||||
@ -993,7 +993,6 @@ Do you want to switch to this account?</string>
|
||||
<string name="delete_x">Delete %s</string>
|
||||
<string name="failed_to_decrypt_cipher_contact_support">Failed to decrypt cipher. Contact support.</string>
|
||||
<string name="decryption_error">Decryption error</string>
|
||||
<string name="add_item_rotation">Add Item Rotation</string>
|
||||
<string name="scan_a_qr_code">Scan a QR code</string>
|
||||
<string name="cannot_add_authenticator_key">Cannot add authenticator key?</string>
|
||||
<string name="enable_camera_permission_to_use_the_scanner">Enable camera permission to use the scanner</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user