From 6b025832d7ce8008add9bbd9918965d4d043dd72 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Sat, 13 Apr 2024 20:39:03 -0400 Subject: [PATCH] DIsplay base settings screen (#25) --- .../authenticator/AuthenticatorNavigation.kt | 44 ------ .../itemlisting/ItemListingGraphNavigation.kt | 2 + .../navbar/AuthenticatorNavBarScreen.kt | 16 +- .../appbar/BitwardenMediumTopAppBar.kt | 87 +++++++++++ .../feature/settings/SettingsNavigation.kt | 35 +++++ .../feature/settings/SettingsScreen.kt | 141 ++++++++++++++++++ .../feature/settings/SettingsViewModel.kt | 54 +++++++ app/src/main/res/drawable/ic_more.xml | 15 ++ .../main/res/drawable/ic_navigate_next.xml | 9 ++ .../res/drawable/ic_verification_codes.xml | 19 ++- 10 files changed, 358 insertions(+), 64 deletions(-) create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsNavigation.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt create mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt create mode 100644 app/src/main/res/drawable/ic_more.xml create mode 100644 app/src/main/res/drawable/ic_navigate_next.xml diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/authenticator/AuthenticatorNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/authenticator/AuthenticatorNavigation.kt index 2a10ccad70..1946ce24ee 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/authenticator/AuthenticatorNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/authenticator/AuthenticatorNavigation.kt @@ -58,49 +58,5 @@ fun NavGraphBuilder.authenticatorGraph( navController.navigateToEditItem(itemId = it) } ) - itemListingDestination( - onNavigateBack = { navController.popBackStack() }, - onNavigateToQrCodeScanner = { navController.navigateToQrCodeScanScreen() }, - onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() }, - onNavigateToEditItemScreen = { navController.navigateToEditItem(itemId = it) }, - onNavigateToSyncWithBitwardenScreen = { - Toast - .makeText( - navController.context, - R.string.not_yet_implemented, - Toast.LENGTH_SHORT, - ) - .show() - /*navController.navigateToSyncWithBitwardenScreen()*/ - }, - onNavigateToImportScreen = { - Toast - .makeText( - navController.context, - R.string.not_yet_implemented, - Toast.LENGTH_SHORT, - ) - .show() - /*navController.navigateToImportScreen()*/ - }, - onNavigateToSearch = { navController.navigateToSearch() }, - ) - editItemDestination( - onNavigateBack = { navController.popBackStack() }, - ) - qrCodeScanDestination( - onNavigateBack = { navController.popBackStack() }, - onNavigateToManualCodeEntryScreen = { - navController.popBackStack() - navController.navigateToManualCodeEntryScreen() - }, - ) - manualCodeEntryDestination( - onNavigateBack = { navController.popBackStack() }, - onNavigateToQrCodeScreen = { - navController.popBackStack() - navController.navigateToQrCodeScanScreen() - } - ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt index 9914766bf6..c994661508 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingGraphNavigation.kt @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.nav import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.qrCodeScanDestination import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.search.itemSearchDestination import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.search.navigateToSearch +import com.x8bit.bitwarden.authenticator.ui.platform.feature.settings.settingsGraph const val ITEM_LISTING_GRAPH_ROUTE = "item_listing_graph" @@ -59,6 +60,7 @@ fun NavGraphBuilder.itemListingGraph( navController.navigateToQrCodeScanScreen() } ) + settingsGraph(navController) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/navbar/AuthenticatorNavBarScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/navbar/AuthenticatorNavBarScreen.kt index 4a4e299042..75e89d6570 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/navbar/AuthenticatorNavBarScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/authenticator/feature/navbar/AuthenticatorNavBarScreen.kt @@ -45,6 +45,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.x8bit.bitwarden.authenticator.R import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ITEM_LISTING_GRAPH_ROUTE +import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ITEM_LIST_ROUTE import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.navigateToItemListGraph import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect @@ -52,6 +53,8 @@ import com.x8bit.bitwarden.authenticator.ui.platform.base.util.max import com.x8bit.bitwarden.authenticator.ui.platform.base.util.toDp import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.authenticator.ui.platform.components.scrim.BitwardenAnimatedScrim +import com.x8bit.bitwarden.authenticator.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE +import com.x8bit.bitwarden.authenticator.ui.platform.feature.settings.navigateToSettingsGraph import com.x8bit.bitwarden.authenticator.ui.platform.theme.RootTransitionProviders import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -74,14 +77,7 @@ fun AuthenticatorNavBarScreen( val navOptions = navController.authenticatorNavBarScreenNavOptions() when (event) { AuthenticatorNavBarEvent.NavigateToSettings -> { - Toast - .makeText( - navController.context, - R.string.not_yet_implemented, - Toast.LENGTH_SHORT - ) - .show() - /* navigateToSettingGraph() */ + navigateToSettingsGraph(navOptions) } AuthenticatorNavBarEvent.NavigateToVerificationCodes -> { @@ -297,7 +293,7 @@ private sealed class AuthenticatorNavBarTab : Parcelable { override val iconRes get() = R.drawable.ic_verification_codes override val labelRes get() = R.string.verification_codes override val contentDescriptionRes get() = R.string.verification_codes - override val route get() = ITEM_LISTING_GRAPH_ROUTE + override val route get() = ITEM_LIST_ROUTE override val testTag get() = "VerificationCodesTab" } @@ -311,7 +307,7 @@ private sealed class AuthenticatorNavBarTab : Parcelable { override val labelRes get() = R.string.settings override val contentDescriptionRes get() = R.string.settings // TODO: Replace with constant when settings screen is complete. - override val route get() = "settings_graph" + override val route get() = SETTINGS_GRAPH_ROUTE override val testTag get() = "SettingsTab" } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt new file mode 100644 index 0000000000..39a59e5f28 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt @@ -0,0 +1,87 @@ +package com.x8bit.bitwarden.authenticator.ui.platform.components.appbar + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.tooling.preview.Preview +import com.x8bit.bitwarden.authenticator.R + +/** + * A custom Bitwarden-themed medium top app bar with support for actions. + * + * This app bar wraps around Material 3's [MediumTopAppBar] and customizes its appearance + * and behavior according to the app theme. + * It provides a title and an optional set of actions on the trailing side. + * These actions are arranged within a custom action row tailored to the app's design requirements. + * + * @param title The text to be displayed as the title of the app bar. + * @param scrollBehavior Defines the scrolling behavior of the app bar. It controls how the app bar + * behaves in conjunction with scrolling content. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BitwardenMediumTopAppBar( + title: String, + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = {}, +) { + MediumTopAppBar( + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onSurface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + scrollBehavior = scrollBehavior, + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.semantics { testTag = "PageTitleLabel" }, + ) + }, + modifier = modifier.semantics { testTag = "HeaderBarComponent" }, + actions = actions, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun BitwardenMediumTopAppBar_preview() { + MaterialTheme { + BitwardenMediumTopAppBar( + title = "Preview Title", + scrollBehavior = TopAppBarDefaults + .exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + ), + actions = { + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_more), + contentDescription = "", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsNavigation.kt new file mode 100644 index 0000000000..989bc8986c --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsNavigation.kt @@ -0,0 +1,35 @@ +package com.x8bit.bitwarden.authenticator.ui.platform.feature.settings + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.navigation +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.composableWithRootPushTransitions + +const val SETTINGS_GRAPH_ROUTE = "settings_graph" +private const val SETTINGS_ROUTE = "settings" + +/** + * Add settings graph to the nav graph. + */ +fun NavGraphBuilder.settingsGraph( + navController: NavController, +) { + navigation( + startDestination = SETTINGS_ROUTE, + route = SETTINGS_GRAPH_ROUTE + ) { + composableWithRootPushTransitions( + route = SETTINGS_ROUTE + ) { + SettingsScreen() + } + } +} + +/** + * Navigate to the settings screen. + */ +fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) { + navigate(SETTINGS_GRAPH_ROUTE, navOptions) +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt new file mode 100644 index 0000000000..d6296ab699 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsScreen.kt @@ -0,0 +1,141 @@ +package com.x8bit.bitwarden.authenticator.ui.platform.feature.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ExperimentalMaterial3Api +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.x8bit.bitwarden.authenticator.R +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.bottomDivider +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.mirrorIfRtl +import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar +import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold +import com.x8bit.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme + +/** + * Display the settings screen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), +) { + + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + + BitwardenScaffold( + topBar = { + BitwardenMediumTopAppBar( + title = stringResource(id = R.string.settings), + scrollBehavior = scrollBehavior + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(state = rememberScrollState()) + ) { + Settings.entries.forEach { + SettingsRow( + text = it.text, + onClick = remember(viewModel) { + { viewModel.trySendAction(SettingsAction.SettingsClick(it)) } + }, + modifier = Modifier + .semantics { testTag = it.testTag } + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun SettingsRow( + text: Text, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + onClick = onClick, + ) + .bottomDivider(paddingStart = 16.dp) + .defaultMinSize(minHeight = 56.dp) + .padding(end = 8.dp, top = 8.dp, bottom = 8.dp) + .then(modifier), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .padding(end = 16.dp) + .weight(1f), + text = text(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Icon( + painter = painterResource(id = R.drawable.ic_navigate_next), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .mirrorIfRtl() + .size(24.dp), + ) + } +} + +@Preview +@Composable +private fun SettingsRows_preview() { + AuthenticatorTheme { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + ) { + Settings.entries.forEach { + SettingsRow( + text = it.text, + onClick = { }, + ) + } + } + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt new file mode 100644 index 0000000000..3040aebf06 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/authenticator/ui/platform/feature/settings/SettingsViewModel.kt @@ -0,0 +1,54 @@ +package com.x8bit.bitwarden.authenticator.ui.platform.feature.settings + +import androidx.compose.material3.Text +import com.x8bit.bitwarden.authenticator.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +/** + * View model for the settings screen. + */ +@HiltViewModel +class SettingsViewModel @Inject constructor( +) : BaseViewModel( + initialState = Unit +) { + override fun handleAction(action: SettingsAction) { + when (action) { + is SettingsAction.SettingsClick -> handleSettingClick(action) + } + } + + private fun handleSettingClick(action: SettingsAction.SettingsClick) { + when (action.setting) { + else -> {} + } + } +} + +/** + * Models events for the settings screen. + */ +sealed class SettingsEvent + +/** + * Models actions for the settings screen. + */ +sealed class SettingsAction { + /** + * User clicked a settings row. + */ + class SettingsClick(val setting: Settings) : SettingsAction() +} + +/** + * Enum representing the settings rows, such as "import" or "export". + * + * @property text The [Text] of the string that represents the label of each setting. + * @property testTag The value that should be set for the test tag. This is used in Appium testing. + */ +enum class Settings( + val text: Text, + val testTag: String, +) diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000000..2cf873f58e --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_navigate_next.xml b/app/src/main/res/drawable/ic_navigate_next.xml new file mode 100644 index 0000000000..7ff13e39f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_navigate_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_verification_codes.xml b/app/src/main/res/drawable/ic_verification_codes.xml index 3a9a4cdde3..9717383a8f 100644 --- a/app/src/main/res/drawable/ic_verification_codes.xml +++ b/app/src/main/res/drawable/ic_verification_codes.xml @@ -1,14 +1,13 @@ - - - - + android:viewportHeight="24" + android:viewportWidth="24"> + + + +