BIT-457: Add Vault Settings and Folders screen UI (#457)

This commit is contained in:
Caleb Derosier
2024-06-20 17:08:07 +01:00
committed by Álison Fernandes
parent 78461394f3
commit c94b303abc
15 changed files with 530 additions and 164 deletions
@@ -27,6 +27,7 @@ private const val SETTINGS_ROUTE: String = "settings"
fun NavGraphBuilder.settingsGraph(
navController: NavController,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
) {
navigation(
startDestination = SETTINGS_ROUTE,
@@ -52,12 +53,15 @@ fun NavGraphBuilder.settingsGraph(
appearanceDestination(onNavigateBack = { navController.popBackStack() })
autoFillDestination(onNavigateBack = { navController.popBackStack() })
otherDestination(onNavigateBack = { navController.popBackStack() })
vaultSettingsDestination(onNavigateBack = { navController.popBackStack() })
vaultSettingsDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToFolders = onNavigateToFolders,
)
}
}
/**
* Navigate to the settings screen screen.
* Navigate to the settings screen.
*/
fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) {
navigate(SETTINGS_GRAPH_ROUTE, navOptions)
@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val FOLDERS_ROUTE = "settings_folders"
/**
* Add folders destinations to the nav graph.
*/
fun NavGraphBuilder.foldersDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = FOLDERS_ROUTE,
) {
FoldersScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the folders screen.
*/
fun NavController.navigateToFolders(navOptions: NavOptions? = null) {
navigate(FOLDERS_ROUTE, navOptions)
}
@@ -0,0 +1,108 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
/**
* Displays the folders screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FoldersScreen(
onNavigateBack: () -> Unit,
viewModel: FoldersViewModel = hiltViewModel(),
) {
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
FoldersEvent.NavigateBack -> onNavigateBack()
is FoldersEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.folders),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(FoldersAction.CloseButtonClick) }
},
)
},
floatingActionButton = {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.primaryContainer,
onClick = remember(viewModel) {
{ viewModel.trySendAction(FoldersAction.AddFolderButtonClick) }
},
modifier = Modifier.navigationBarsPadding(),
) {
Icon(
painter = painterResource(id = R.drawable.ic_plus),
contentDescription = stringResource(id = R.string.add_item),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
) {
// TODO BIT-460 populate Folders screen
Text(
text = stringResource(id = R.string.no_folders_to_list),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.fillMaxSize()
.padding(
vertical = 4.dp,
horizontal = 16.dp,
),
)
}
}
}
@@ -0,0 +1,60 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* View model for the folders screen.
*/
@HiltViewModel
class FoldersViewModel @Inject constructor() :
BaseViewModel<Unit, FoldersEvent, FoldersAction>(
initialState = Unit,
) {
override fun handleAction(action: FoldersAction): Unit = when (action) {
FoldersAction.AddFolderButtonClick -> handleAddFolderButtonClicked()
FoldersAction.CloseButtonClick -> handleCloseButtonClicked()
}
private fun handleAddFolderButtonClicked() {
// TODO BIT-458 implement add folders
sendEvent(FoldersEvent.ShowToast("Not yet implemented."))
}
private fun handleCloseButtonClicked() {
sendEvent(FoldersEvent.NavigateBack)
}
}
/**
* Models events for the folders screen.
*/
sealed class FoldersEvent {
/**
* Navigates back to the previous screen.
*/
data object NavigateBack : FoldersEvent()
/**
* Shows a toast with the given [message].
*/
data class ShowToast(
val message: String,
) : FoldersEvent()
}
/**
* Models actions for the folders screen.
*/
sealed class FoldersAction {
/**
* Indicates that the user clicked the add folder button.
*/
data object AddFolderButtonClick : FoldersAction()
/**
* Indicates that the user clicked the close button.
*/
data object CloseButtonClick : FoldersAction()
}
@@ -12,11 +12,15 @@ private const val VAULT_SETTINGS_ROUTE = "vault_settings"
*/
fun NavGraphBuilder.vaultSettingsDestination(
onNavigateBack: () -> Unit,
onNavigateToFolders: () -> Unit,
) {
composableWithPushTransitions(
route = VAULT_SETTINGS_ROUTE,
) {
VaultSettingsScreen(onNavigateBack = onNavigateBack)
VaultSettingsScreen(
onNavigateBack = onNavigateBack,
onNavigateToFolders = onNavigateToFolders,
)
}
}
@@ -1,7 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -12,26 +14,36 @@ import androidx.compose.runtime.Composable
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
/**
* Displays the vault screen.
* Displays the vault settings screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VaultSettingsScreen(
onNavigateBack: () -> Unit,
onNavigateToFolders: () -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(),
) {
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
VaultSettingsEvent.NavigateBack -> onNavigateBack.invoke()
VaultSettingsEvent.NavigateBack -> onNavigateBack()
VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders()
is VaultSettingsEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
}
}
@@ -58,7 +70,34 @@ fun VaultSettingsScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
// TODO: BIT-928 Display Vault UI
BitwardenTextRow(
text = stringResource(R.string.folders),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.FoldersButtonClick) }
},
withDivider = true,
modifier = Modifier.fillMaxWidth(),
)
BitwardenTextRow(
text = stringResource(R.string.export_vault),
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) }
},
withDivider = true,
modifier = Modifier.fillMaxWidth(),
)
BitwardenExternalLinkRow(
text = stringResource(R.string.import_items),
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
dialogTitle = stringResource(id = R.string.import_items_confirmation),
dialogMessage = stringResource(id = R.string.import_items_description),
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@@ -13,7 +13,28 @@ class VaultSettingsViewModel @Inject constructor() :
initialState = Unit,
) {
override fun handleAction(action: VaultSettingsAction): Unit = when (action) {
VaultSettingsAction.BackClick -> sendEvent(VaultSettingsEvent.NavigateBack)
VaultSettingsAction.BackClick -> handleBackClicked()
VaultSettingsAction.ExportVaultClick -> handleExportVaultClicked()
VaultSettingsAction.FoldersButtonClick -> handleFoldersButtonClicked()
VaultSettingsAction.ImportItemsClick -> handleImportItemsClicked()
}
private fun handleBackClicked() {
sendEvent(VaultSettingsEvent.NavigateBack)
}
private fun handleExportVaultClicked() {
// TODO BIT-1272 go to vault export screen
sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented."))
}
private fun handleFoldersButtonClicked() {
sendEvent(VaultSettingsEvent.NavigateToFolders)
}
private fun handleImportItemsClicked() {
// TODO BIT-972 implement import items functionality
sendEvent(VaultSettingsEvent.ShowToast("Not yet implemented."))
}
}
@@ -25,6 +46,18 @@ sealed class VaultSettingsEvent {
* Navigate back.
*/
data object NavigateBack : VaultSettingsEvent()
/**
* Navigate to the Folders screen.
*/
data object NavigateToFolders : VaultSettingsEvent()
/**
* Shows a toast with the given [message].
*/
data class ShowToast(
val message: String,
) : VaultSettingsEvent()
}
/**
@@ -35,4 +68,19 @@ sealed class VaultSettingsAction {
* User clicked back button.
*/
data object BackClick : VaultSettingsAction()
/**
* Indicates that the user clicked the Export Vault button.
*/
data object ExportVaultClick : VaultSettingsAction()
/**
* Indicates that the user clicked the Folders button.
*/
data object FoldersButtonClick : VaultSettingsAction()
/**
* Indicates that the user clicked the Import Items button.
*/
data object ImportItemsClick : VaultSettingsAction()
}
@@ -6,6 +6,8 @@ import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
import com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory.navigateToPasswordHistory
@@ -38,6 +40,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
route = VAULT_UNLOCKED_GRAPH_ROUTE,
) {
vaultUnlockedNavBarDestination(
onNavigateToFolders = { navController.navigateToFolders() },
onNavigateToVaultAddItem = {
navController.navigateToVaultAddEditItem(VaultAddEditType.AddItem)
},
@@ -59,5 +62,6 @@ fun NavGraphBuilder.vaultUnlockedGraph(
)
newSendDestination(onNavigateBack = { navController.popBackStack() })
passwordHistoryDestination(onNavigateBack = { navController.popBackStack() })
foldersDestination(onNavigateBack = { navController.popBackStack() })
}
}
@@ -27,6 +27,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
onNavigateToNewSend: () -> Unit,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
) {
composableWithStayTransitions(
@@ -38,6 +39,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToVaultEditItem = onNavigateToVaultEditItem,
onNavigateToNewSend = onNavigateToNewSend,
onNavigateToDeleteAccount = onNavigateToDeleteAccount,
onNavigateToFolders = onNavigateToFolders,
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
)
}
@@ -62,6 +62,7 @@ import kotlinx.parcelize.Parcelize
/**
* Top level composable for the Vault Unlocked Screen.
*/
@Suppress("LongParameterList")
@Composable
fun VaultUnlockedNavBarScreen(
viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(),
@@ -71,6 +72,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
onNavigateToNewSend: () -> Unit,
onNavigateToDeleteAccount: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToPasswordHistory: () -> Unit,
) {
EventsEffect(viewModel = viewModel) { event ->
@@ -102,6 +104,7 @@ fun VaultUnlockedNavBarScreen(
navigateToVaultAddItem = onNavigateToVaultAddItem,
navigateToNewSend = onNavigateToNewSend,
navigateToDeleteAccount = onNavigateToDeleteAccount,
navigateToFolders = onNavigateToFolders,
navigateToPasswordHistory = onNavigateToPasswordHistory,
generatorTabClickedAction = {
viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick)
@@ -134,6 +137,7 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit,
navigateToNewSend: () -> Unit,
navigateToDeleteAccount: () -> Unit,
navigateToFolders: () -> Unit,
navigateToPasswordHistory: () -> Unit,
) {
var shouldDimNavBar by remember { mutableStateOf(false) }
@@ -200,6 +204,7 @@ private fun VaultUnlockedNavBarScaffold(
settingsGraph(
navController = navController,
onNavigateToDeleteAccount = navigateToDeleteAccount,
onNavigateToFolders = navigateToFolders,
)
}
}