BIT-904: Reskin for the first time user vault screen (#144)

This commit is contained in:
joshua-livefront 2023-10-23 14:25:18 -04:00 committed by Álison Fernandes
parent db30504b70
commit 284cd9ab54
14 changed files with 382 additions and 66 deletions

View file

@ -1,11 +1,14 @@
package com.x8bit.bitwarden.ui.platform.base.util package com.x8bit.bitwarden.ui.platform.base.util
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
/** /**
* A function for converting pixels to [Dp] within a composable function. * A function for converting pixels to [Dp] within a composable function.
@ -27,3 +30,16 @@ fun IntSize.toDpSize(density: Density): DpSize = with(density) {
height = height.toDp(), height = height.toDp(),
) )
} }
/**
* Converts a [Dp] value to [TextUnit] with [TextUnitType.Sp] as its type.
*
* This allows for easier conversion between density-independent pixels (dp) and
* scalable pixels (sp) when setting text sizes in Compose. For example, a dp value
* representing a size can be directly used for text styling.
*/
@Composable
fun Dp.toUnscaledTextUnit(): TextUnit {
val scalingFactor = LocalConfiguration.current.fontScale
return TextUnit(value / scalingFactor, TextUnitType.Sp)
}

View file

@ -0,0 +1,73 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.toUnscaledTextUnit
/**
* Displays an icon representing a Bitwarden account with the user's initials superimposed.
* The icon is typically a colored circle with the initials centered on it.
*
* @param initials The initials of the user to be displayed on top of the icon.
* @param color The color to be applied as the tint for the icon.
* @param onClick An action to be invoked when the icon is clicked.
*/
@Composable
fun BitwardenAccountActionItem(
initials: String,
color: Color,
onClick: () -> Unit,
) {
val iconPainter = painterResource(id = R.drawable.ic_account_initials_container)
val contentDescription = stringResource(id = R.string.account)
Box(
contentAlignment = Alignment.Center,
) {
IconButton(onClick = onClick) {
Icon(
painter = iconPainter,
contentDescription = contentDescription,
tint = color,
)
}
Text(
text = initials,
style = TextStyle(
fontSize = 11.dp.toUnscaledTextUnit(),
lineHeight = 13.dp.toUnscaledTextUnit(),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W400,
),
color = colorResource(id = R.color.white),
)
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenAccountActionItem_preview() {
val mockInitials = "BW"
val mockColor = colorResource(id = R.color.primary)
BitwardenAccountActionItem(
initials = mockInitials,
color = mockColor,
onClick = {},
)
}

View file

@ -1,50 +1,42 @@
package com.x8bit.bitwarden.ui.platform.components package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
/** /**
* A custom Bitwarden-themed large top app bar with an overflow menu action. * A custom Bitwarden-themed medium top app bar with support for actions.
* *
* This app bar wraps around the Material 3's [LargeTopAppBar] and customizes its appearance * This app bar wraps around Material 3's [MediumTopAppBar] and customizes its appearance
* and behavior according to the app theme. * and behavior according to the app theme.
* It provides a title and an optional overflow menu, represented by a dropdown containing * It provides a title and an optional set of actions on the trailing side.
* a set of menu items. * 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 title The text to be displayed as the title of the app bar.
* @param dropdownMenuItemContent A single overflow menu in the right with contents
* defined by the [dropdownMenuItemContent]. It is strongly recommended that this content
* be a stack of [DropdownMenuItem].
* @param scrollBehavior Defines the scrolling behavior of the app bar. It controls how 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. * 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BitwardenMediumTopAppBar( fun BitwardenMediumTopAppBar(
title: String, title: String,
dropdownMenuItemContent: @Composable ColumnScope.() -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
actions: @Composable RowScope.() -> Unit = {},
) { ) {
var isOverflowMenuVisible by remember { mutableStateOf(false) }
MediumTopAppBar( MediumTopAppBar(
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface,
@ -57,21 +49,30 @@ fun BitwardenMediumTopAppBar(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
}, },
actions = { actions = actions,
Box { )
IconButton(onClick = { isOverflowMenuVisible = !isOverflowMenuVisible }) { }
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
private fun BitwardenMediumTopAppBar_preview() {
MaterialTheme {
BitwardenMediumTopAppBar(
title = "Preview Title",
scrollBehavior = TopAppBarDefaults
.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState(),
),
actions = {
IconButton(onClick = { }) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_more), painter = painterResource(id = R.drawable.ic_more),
contentDescription = stringResource(id = R.string.more), contentDescription = "",
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
DropdownMenu( },
expanded = isOverflowMenuVisible, )
onDismissRequest = { isOverflowMenuVisible = false }, }
content = dropdownMenuItemContent,
)
}
},
)
} }

View file

@ -0,0 +1,61 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Represents a composable overflow item specifically tailored for Bitwarden's UI.
*
* This composable wraps an [IconButton] with an "overflow" icon, typically used to
* indicate more actions available that are not immediately visible on the interface.
* The item is centrally aligned within a predefined [Box] of size 24.dp.
*
* @param dropdownMenuItemContent A single overflow menu in the right with contents
* defined by the [dropdownMenuItemContent]. It is strongly recommended that this content
* be a stack of [DropdownMenuItem].
*/
@Composable
fun BitwardenOverflowActionItem(
dropdownMenuItemContent: @Composable ColumnScope.() -> Unit = {},
) {
var isOverflowMenuVisible by remember { mutableStateOf(false) }
Box(
contentAlignment = Alignment.Center,
) {
IconButton(onClick = { isOverflowMenuVisible = !isOverflowMenuVisible }) {
Icon(
painter = painterResource(id = R.drawable.ic_more),
contentDescription = stringResource(id = R.string.more),
tint = MaterialTheme.colorScheme.onSurface,
)
}
DropdownMenu(
expanded = isOverflowMenuVisible,
onDismissRequest = { isOverflowMenuVisible = false },
content = dropdownMenuItemContent,
)
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenOverflowActionItem_preview() {
BitwardenTheme {
BitwardenOverflowActionItem(dropdownMenuItemContent = {})
}
}

View file

@ -0,0 +1,42 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
/**
* Represents the Bitwarden search action item.
*
* This is an [Icon] composable tailored specifically for the search functionality
* in the Bitwarden 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 BitwardenSearchActionItem(
contentDescription: String,
onClick: () -> Unit,
) {
IconButton(
onClick = onClick,
) {
Icon(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = contentDescription,
)
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenSearchActionItem_preview() {
BitwardenSearchActionItem(
contentDescription = "Search",
onClick = {},
)
}

View file

@ -49,6 +49,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.toDp import com.x8bit.bitwarden.ui.platform.base.util.toDp
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithTwoIcons import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithTwoIcons
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
@ -110,6 +111,9 @@ fun GeneratorScreen(viewModel: GeneratorViewModel = hiltViewModel()) {
BitwardenMediumTopAppBar( BitwardenMediumTopAppBar(
title = stringResource(id = R.string.generator), title = stringResource(id = R.string.generator),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
actions = {
BitwardenOverflowActionItem()
},
) )
}, },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),

View file

@ -7,10 +7,10 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
/** /**
* Loading view for the [VaultScreen]. * Loading view for the [VaultScreen].
@ -20,7 +20,7 @@ fun VaultLoadingView(paddingValues: PaddingValues) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.White) .background(MaterialTheme.colorScheme.surface)
.padding(paddingValues), .padding(paddingValues),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -1,16 +1,23 @@
package com.x8bit.bitwarden.ui.vault.feature.vault package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
@ -25,19 +32,36 @@ fun VaultNoItemsView(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues)
.background(color = MaterialTheme.colorScheme.surface),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
modifier = Modifier.padding(16.dp), textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = stringResource(id = R.string.no_items), text = stringResource(id = R.string.no_items),
style = MaterialTheme.typography.bodyMedium,
) )
Spacer(modifier = Modifier.height(24.dp))
Button( Button(
modifier = Modifier.padding(16.dp), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = addItemClickAction, onClick = addItemClickAction,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
) { ) {
Text(text = stringResource(id = R.string.add_an_item)) Text(
text = stringResource(id = R.string.add_an_item),
style = MaterialTheme.typography.labelLarge,
)
} }
} }
} }

View file

@ -4,24 +4,32 @@ import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.material.icons.Icons import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
/** /**
* The vault screen for the application. * The vault screen for the application.
@ -56,6 +64,8 @@ fun VaultScreen(
/** /**
* Scaffold for the [VaultScreen] * Scaffold for the [VaultScreen]
*/ */
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun VaultScreenScaffold( private fun VaultScreenScaffold(
state: VaultState, state: VaultState,
@ -66,11 +76,26 @@ private fun VaultScreenScaffold(
var accountMenuVisible by rememberSaveable { var accountMenuVisible by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
topBar = { topBar = {
VaultTopBar( BitwardenMediumTopAppBar(
accountIconClickAction = { accountMenuVisible = !accountMenuVisible }, title = stringResource(id = R.string.my_vault),
searchIconClickAction = searchIconClickAction, scrollBehavior = scrollBehavior,
actions = {
BitwardenAccountActionItem(
initials = state.initials,
color = state.avatarColor,
onClick = { accountMenuVisible = !accountMenuVisible },
)
BitwardenSearchActionItem(
contentDescription = stringResource(id = R.string.search_vault),
onClick = searchIconClickAction,
)
BitwardenOverflowActionItem()
},
) )
}, },
floatingActionButton = { floatingActionButton = {
@ -81,22 +106,23 @@ private fun VaultScreenScaffold(
enter = fadeIn() + expandIn { IntSize(width = 1, height = 1) }, enter = fadeIn() + expandIn { IntSize(width = 1, height = 1) },
) { ) {
FloatingActionButton( FloatingActionButton(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primaryContainer,
onClick = addItemClickAction, onClick = addItemClickAction,
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Add, painter = painterResource(id = R.drawable.ic_plus),
contentDescription = stringResource(id = R.string.add_item), contentDescription = stringResource(id = R.string.add_item),
tint = MaterialTheme.colorScheme.onPrimary, tint = MaterialTheme.colorScheme.onPrimaryContainer,
) )
} }
} }
}, },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
when (state) { when (state.viewState) {
is VaultState.Content -> VaultContentView(paddingValues = paddingValues) is VaultState.ViewState.Content -> VaultContentView(paddingValues = paddingValues)
is VaultState.Loading -> VaultLoadingView(paddingValues = paddingValues) is VaultState.ViewState.Loading -> VaultLoadingView(paddingValues = paddingValues)
is VaultState.NoItems -> VaultNoItemsView( is VaultState.ViewState.NoItems -> VaultNoItemsView(
paddingValues = paddingValues, paddingValues = paddingValues,
addItemClickAction = addItemClickAction, addItemClickAction = addItemClickAction,
) )

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.vault package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -13,7 +14,12 @@ import javax.inject.Inject
*/ */
@HiltViewModel @HiltViewModel
class VaultViewModel @Inject constructor() : BaseViewModel<VaultState, VaultEvent, VaultAction>( class VaultViewModel @Inject constructor() : BaseViewModel<VaultState, VaultEvent, VaultAction>(
initialState = VaultState.Loading, // TODO retrieve this from the data layer BIT-205
initialState = VaultState(
initials = "BW",
avatarColor = Color.Blue,
viewState = VaultState.ViewState.Loading,
),
) { ) {
init { init {
@ -21,7 +27,9 @@ class VaultViewModel @Inject constructor() : BaseViewModel<VaultState, VaultEven
// TODO will need to load actual vault items BIT-205 // TODO will need to load actual vault items BIT-205
@Suppress("MagicNumber") @Suppress("MagicNumber")
delay(2000) delay(2000)
mutableStateFlow.update { VaultState.NoItems } mutableStateFlow.update { currentState ->
currentState.copy(viewState = VaultState.ViewState.NoItems)
}
} }
} }
@ -44,23 +52,40 @@ class VaultViewModel @Inject constructor() : BaseViewModel<VaultState, VaultEven
} }
/** /**
* Models state for the [VaultScreen]. * Represents the overall state for the [VaultScreen].
*
* @property avatarColor The color of the avatar in HEX format.
* @property initials The initials to be displayed on the avatar.
* @property viewState The specific view state representing loading, no items, or content state.
*/ */
sealed class VaultState { data class VaultState(
/** val avatarColor: Color,
* Loading state for the [VaultScreen]. val initials: String,
*/ val viewState: ViewState,
data object Loading : VaultState() ) {
/** /**
* No items state for the [VaultScreen]. * Represents the specific view states for the [VaultScreen].
*/ */
data object NoItems : VaultState() sealed class ViewState {
/** /**
* Content state for the [VaultScreen]. * Loading state for the [VaultScreen], signifying that the content is being processed.
*/ */
data class Content(val itemList: List<String>) : VaultState() data object Loading : ViewState()
/**
* Represents a state where the [VaultScreen] has no items to display.
*/
data object NoItems : ViewState()
/**
* Content state for the [VaultScreen] showing the actual content or items.
*
* @property itemList The list of items to be displayed in the [VaultScreen].
*/
data class Content(val itemList: List<String>) : ViewState()
}
} }
/** /**

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"
android:fillColor="#5ED378"
android:fillAlpha="0.74"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M16.163,8.082C16.163,8.082 16.163,8.082 16.163,8.082C16.163,3.618 12.545,0 8.082,0C3.618,0 0,3.618 0,8.082C0,12.545 3.618,16.163 8.082,16.163C8.082,16.163 8.082,16.163 8.082,16.163C9.142,16.163 10.154,15.959 11.082,15.588C11.14,15.568 11.192,15.566 11.225,15.571C11.242,15.574 11.25,15.577 11.253,15.579L15.128,19.454C15.857,20.182 17.038,20.182 17.766,19.454L19.463,17.756C20.192,17.028 20.192,15.847 19.463,15.119L15.584,11.24C15.583,11.236 15.579,11.228 15.577,11.212C15.571,11.178 15.573,11.127 15.593,11.069C15.961,10.145 16.163,9.137 16.163,8.082ZM14.442,10.598C13.751,12.344 12.361,13.737 10.618,14.434C10.615,14.436 10.611,14.437 10.607,14.439C10.358,14.538 10.102,14.622 9.839,14.692C9.33,14.827 8.797,14.905 8.248,14.918C8.193,14.919 8.137,14.92 8.082,14.92C4.305,14.92 1.243,11.858 1.243,8.082C1.243,4.305 4.305,1.243 8.082,1.243C11.858,1.243 14.92,4.305 14.92,8.082C14.92,8.082 14.92,8.082 14.92,8.082C14.92,8.609 14.86,9.122 14.748,9.615C14.74,9.648 14.732,9.68 14.724,9.713C14.65,10.016 14.556,10.31 14.443,10.596M12.368,14.935C13.409,14.282 14.291,13.398 14.942,12.356L18.584,15.998C18.827,16.241 18.827,16.634 18.584,16.877L16.887,18.575C16.644,18.817 16.25,18.817 16.008,18.575L12.368,14.935Z"
android:fillColor="#1B1B1F"
android:fillType="evenOdd"/>
</group>
</vector>

Binary file not shown.

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.vault package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
@ -17,7 +18,13 @@ class VaultScreenTest : BaseComposeTest() {
fun `search icon click should send SearchIconClick action`() { fun `search icon click should send SearchIconClick action`() {
val viewModel = mockk<VaultViewModel>(relaxed = true) { val viewModel = mockk<VaultViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow() every { eventFlow } returns emptyFlow()
every { stateFlow } returns MutableStateFlow(VaultState.NoItems) every { stateFlow } returns MutableStateFlow(
VaultState(
avatarColor = Color.Blue,
initials = "BW",
viewState = VaultState.ViewState.NoItems,
),
)
} }
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
@ -34,7 +41,13 @@ class VaultScreenTest : BaseComposeTest() {
fun `floating action button click should send AddItemClick action`() { fun `floating action button click should send AddItemClick action`() {
val viewModel = mockk<VaultViewModel>(relaxed = true) { val viewModel = mockk<VaultViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow() every { eventFlow } returns emptyFlow()
every { stateFlow } returns MutableStateFlow(VaultState.NoItems) every { stateFlow } returns MutableStateFlow(
VaultState(
avatarColor = Color.Blue,
initials = "BW",
viewState = VaultState.ViewState.NoItems,
),
)
} }
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
@ -51,8 +64,15 @@ class VaultScreenTest : BaseComposeTest() {
fun `add an item button click should send AddItemClick action`() { fun `add an item button click should send AddItemClick action`() {
val viewModel = mockk<VaultViewModel>(relaxed = true) { val viewModel = mockk<VaultViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow() every { eventFlow } returns emptyFlow()
every { stateFlow } returns MutableStateFlow(VaultState.NoItems) every { stateFlow } returns MutableStateFlow(
VaultState(
avatarColor = Color.Blue,
initials = "BW",
viewState = VaultState.ViewState.NoItems,
),
)
} }
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
VaultScreen( VaultScreen(