mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 01:16:02 +03:00
BIT-904: Reskin for the first time user vault screen (#144)
This commit is contained in:
parent
db30504b70
commit
284cd9ab54
14 changed files with 382 additions and 66 deletions
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
10
app/src/main/res/drawable/ic_account_initials_container.xml
Normal file
10
app/src/main/res/drawable/ic_account_initials_container.xml
Normal 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>
|
14
app/src/main/res/drawable/ic_search.xml
Normal file
14
app/src/main/res/drawable/ic_search.xml
Normal 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>
|
BIN
app/src/main/res/font/sf_pro.ttf
Normal file
BIN
app/src/main/res/font/sf_pro.ttf
Normal file
Binary file not shown.
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue