From 4c92ee0b23f49e9e8665133100c115f1e7c3db04 Mon Sep 17 00:00:00 2001
From: joshua-livefront <139182194+joshua-livefront@users.noreply.github.com>
Date: Mon, 11 Sep 2023 10:16:18 -0400
Subject: [PATCH] [BIT-148] Adding wireframe UI for password generator screen
(#37)
---
.../feature/generator/GeneratorScreen.kt | 273 ++++++++++++++++++
app/src/main/res/values/strings.xml | 21 ++
2 files changed, 294 insertions(+)
create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt
new file mode 100644
index 000000000..a0a2054e8
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt
@@ -0,0 +1,273 @@
+package com.x8bit.bitwarden.ui.tools.feature.generator
+
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
+
+/**
+ * Top level composable for the generator screen.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GeneratorScreen() {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ titleContentColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ title = {
+ Text(
+ text = stringResource(id = R.string.generator_label),
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+ },
+ navigationIcon = {
+ Spacer(Modifier.width(40.dp))
+ },
+ actions = {
+ OverflowMenu()
+ },
+ )
+ },
+ ) { innerPadding ->
+ ScrollContent(modifier = Modifier.padding(innerPadding))
+ }
+}
+
+@Composable
+private fun OverflowMenu() {
+ IconButton(
+ onClick = {},
+ ) {
+ Icon(
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = stringResource(id = R.string.overflow_menu),
+ tint = MaterialTheme.colorScheme.onPrimary,
+ )
+ }
+}
+
+@Composable
+private fun ScrollContent(modifier: Modifier = Modifier) {
+ LazyColumn(modifier = modifier.fillMaxSize()) {
+ item { DynamicStringItem() }
+ item { TextItem(title = stringResource(id = R.string.generation_prompt)) }
+ item { TextItem(title = stringResource(id = R.string.password_type), showOptions = true) }
+ item { LengthSliderItem() }
+ item { ToggleItem(stringResource(id = R.string.capital_letters_toggle_text)) }
+ item { ToggleItem(stringResource(id = R.string.lowercase_letters_toggle_text)) }
+ item { ToggleItem(stringResource(id = R.string.numbers_toggle_text)) }
+ item { ToggleItem(stringResource(id = R.string.special_characters_toggle_text)) }
+ item { CounterItem(label = stringResource(id = R.string.minimum_numbers)) }
+ item { CounterItem(label = stringResource(id = R.string.minimum_special)) }
+ item { ToggleItem(stringResource(id = R.string.avoid_ambiguous_characters)) }
+ }
+}
+
+@Composable
+private fun DynamicStringItem() {
+ // TODO(BIT-276): Move this state to ViewModel
+ val placeholderPassword = stringResource(id = R.string.placeholder_password)
+ val dynamicString = remember { mutableStateOf(placeholderPassword) }
+
+ Box(modifier = Modifier.padding(horizontal = 16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(text = dynamicString.value)
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconButton(
+ onClick = {},
+ ) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = stringResource(id = R.string.copy),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ IconButton(
+ onClick = {},
+ ) {
+ Icon(
+ imageVector = Icons.Default.Refresh,
+ contentDescription = stringResource(id = R.string.refresh),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun TextItem(title: String, showOptions: Boolean = false) {
+ // TODO(BIT-276): Move this state to ViewModel
+ val defaultType = stringResource(id = R.string.password)
+ val content = remember { mutableStateOf(defaultType) }
+
+ CommonPadding {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(top = 4.dp, bottom = 4.dp),
+ verticalArrangement = Arrangement.Center,
+ ) {
+ if (showOptions) {
+ Text(
+ stringResource(id = R.string.options),
+ style = TextStyle(fontSize = 12.sp),
+ color = MaterialTheme.colorScheme.primary,
+ )
+ }
+ Text(title, style = TextStyle(fontSize = 10.sp))
+ Text(content.value)
+ }
+ }
+}
+
+@Composable
+private fun LengthSliderItem() {
+ // TODO(BIT-276): Move this state to ViewModel
+ val sliderPosition = remember { mutableStateOf(0f) }
+ CommonPadding {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(stringResource(id = R.string.length))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(sliderPosition.value.toInt().toString())
+ Slider(
+ value = sliderPosition.value,
+ onValueChange = {},
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ToggleItem(title: String) {
+ // TODO(BIT-276): Move this state to ViewModel
+ val isToggled = remember { mutableStateOf(false) }
+ CommonPadding {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(title)
+ Switch(checked = isToggled.value, onCheckedChange = { isToggled.value = it })
+ }
+ }
+}
+
+@Composable
+private fun CounterItem(label: String) {
+ // TODO(BIT-276): Move this state to ViewModel
+ val counter = remember { mutableStateOf(1) }
+
+ CommonPadding {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(label)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(counter.value.toString())
+ IconButton(
+ onClick = {},
+ ) {
+ Icon(
+ Icons.Default.ArrowBack,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ IconButton(
+ onClick = {},
+ ) {
+ Icon(
+ Icons.Default.ArrowForward,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CommonPadding(content: @Composable () -> Unit) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ content()
+ Divider()
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun GeneratorPreview() {
+ BitwardenTheme {
+ GeneratorScreen()
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e8c0f8c00..c92184c9a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -18,6 +18,27 @@
New around here?
Remember me
+
+ PLACEHOLDER
+ What would you like to generate?
+ Password
+ Passphrase
+ Password type
+ A–Z
+ a–z
+ 0–9
+ !@#$%^&*
+ Minimum numbers
+ Minimum special
+ Avoid ambiguous characters
+ Copy
+ Refresh
+ Decrease
+ Increase
+ Options
+ Length
+ Overflow menu
+
Press to navigate to the generator screen.
Press to navigate to the send screen.