From 27b98f8d9d1bf70c89843c4f88fd4f331b7187ff Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Fri, 20 Dec 2024 13:23:57 +0200 Subject: [PATCH] improvement(Export): Minor UI changes --- .../composeResources/values/strings.xml | 5 +- .../feature/auth/AccountViewStateProducer.kt | 26 +++ .../keyguard/feature/export/ExportRoute.kt | 15 +- .../keyguard/feature/export/ExportScreen.kt | 55 ++--- .../home/vault/component/VaultViewItem.kt | 1 + .../component/VaultViewQuickActionsItem.kt | 202 ++++++++++++++++++ .../feature/home/vault/model/VaultViewItem.kt | 7 + .../feature/onboarding/OnboardingScreen.kt | 6 + 8 files changed, 283 insertions(+), 34 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewQuickActionsItem.kt diff --git a/common/src/commonMain/composeResources/values/strings.xml b/common/src/commonMain/composeResources/values/strings.xml index 5e7e7c71..d7fd4f90 100644 --- a/common/src/commonMain/composeResources/values/strings.xml +++ b/common/src/commonMain/composeResources/values/strings.xml @@ -977,6 +977,7 @@ Export items Archive password + Export vault as an encrypted ZIP archive. Vault will be kept unlocked during the export process. The attachments are referenced in the vault's data. The attachments are never stored in an unencrypted form in the process. Export attachments Export @@ -1210,7 +1211,9 @@ Show as Barcode Encode a text as different barcodes. Generator - Generate passwords or usernames. + Generate passwords, usernames, emails and keys. + Export + Export Vault's data, including attachments. Data safety Local data diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt index e5cbf854..5afcbf69 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt @@ -569,7 +569,33 @@ private fun buildItemsFlow( } } + val quickActions = VaultViewItem.QuickActions( + id = "quick_actions", + actions = buildContextItems { + section { + this += ExportRoute.actionOrNull( + translator = scope, + accountId = accountId, + individual = true, + navigate = scope::navigate, + ) + this += ExportRoute.actionOrNull( + translator = scope, + accountId = accountId, + individual = false, + navigate = scope::navigate, + ) + } + }, + ) + emit(quickActions) + if (account != null) { + val mainSectionItem = VaultViewItem.Section( + id = "main.section", + ) + emit(mainSectionItem) + val ff0 = VaultViewItem.Action( id = "ciphers", title = scope.translate(Res.string.items), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportRoute.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportRoute.kt index 05bfec20..72ee98db 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportRoute.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportRoute.kt @@ -16,6 +16,7 @@ import com.artemchep.keyguard.feature.navigation.state.TranslatorScope import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.* import com.artemchep.keyguard.ui.FlatItemAction +import com.artemchep.keyguard.ui.icons.ChevronIcon import com.artemchep.keyguard.ui.icons.Stub import com.artemchep.keyguard.ui.icons.icon @@ -50,13 +51,13 @@ data class ExportRoute( translator.translate(res) } return FlatItemAction( - leading = kotlin.run { - val res = if (individual) { - Icons.Outlined.SaveAlt - } else { - Icons.Stub - } - icon(res) + icon = if (individual) { + Icons.Outlined.SaveAlt + } else { + Icons.Stub + }, + trailing = { + ChevronIcon() }, title = TextHolder.Value(title), onClick = { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportScreen.kt index b700e504..edf07104 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/export/ExportScreen.kt @@ -350,21 +350,33 @@ private fun ExportScreen( ) } + Spacer( + modifier = Modifier + .height(32.dp), + ) + Icon( + modifier = Modifier + .padding(horizontal = Dimens.horizontalPadding), + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = LocalContentColor.current.combineAlpha(alpha = MediumEmphasisAlpha), + ) + Spacer( + modifier = Modifier + .height(16.dp), + ) + Text( + modifier = Modifier + .padding(horizontal = Dimens.horizontalPadding), + text = stringResource(Res.string.exportaccount_format_note), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current + .combineAlpha(alpha = MediumEmphasisAlpha), + ) ExpandedIfNotEmpty( Unit.takeIf { attachments?.enabled == true }, ) { Column { - Spacer( - modifier = Modifier - .height(32.dp), - ) - Icon( - modifier = Modifier - .padding(horizontal = Dimens.horizontalPadding), - imageVector = Icons.Outlined.Info, - contentDescription = null, - tint = LocalContentColor.current.combineAlpha(alpha = MediumEmphasisAlpha), - ) Spacer( modifier = Modifier .height(16.dp), @@ -478,21 +490,7 @@ private fun ColumnScope.ExportContentOk( ) FlatItemLayout( leading = { - BadgedBox( - modifier = Modifier - .zIndex(20f), - badge = { - val size = attachments.size - ?: return@BadgedBox - Badge( - containerColor = MaterialTheme.colorScheme.badgeContainer, - ) { - Text(text = size) - } - }, - ) { - Icon(Icons.Outlined.KeyguardAttachment, null) - } + Icon(Icons.Outlined.KeyguardAttachment, null) }, content = { FlatItemTextContent( @@ -501,6 +499,11 @@ private fun ColumnScope.ExportContentOk( text = stringResource(Res.string.exportaccount_include_attachments_title), ) }, + text = { + val size = attachments.size + ?: return@FlatItemTextContent + Text(size) + }, ) }, trailing = { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewItem.kt index 39323e51..86a0ca9f 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewItem.kt @@ -16,6 +16,7 @@ fun VaultViewItem( is VaultViewItem.Error -> VaultViewErrorItem(modifier, item) is VaultViewItem.Info -> VaultViewInfoItem(modifier, item) is VaultViewItem.Identity -> VaultViewIdentityItem(modifier, item) + is VaultViewItem.QuickActions -> VaultViewQuickActionsItem(modifier, item) is VaultViewItem.Action -> VaultViewActionItem(modifier, item) is VaultViewItem.Value -> VaultViewValueItem(modifier, item) is VaultViewItem.Switch -> VaultViewSwitchItem(modifier, item) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewQuickActionsItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewQuickActionsItem.kt new file mode 100644 index 00000000..0f7f5c0b --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewQuickActionsItem.kt @@ -0,0 +1,202 @@ +package com.artemchep.keyguard.feature.home.vault.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +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.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem +import com.artemchep.keyguard.feature.localization.textResource +import com.artemchep.keyguard.ui.ContextItem +import com.artemchep.keyguard.ui.DisabledEmphasisAlpha +import com.artemchep.keyguard.ui.DropdownMinWidth +import com.artemchep.keyguard.ui.FlatItemAction +import com.artemchep.keyguard.ui.FlatItemTextContent +import com.artemchep.keyguard.ui.icons.Stub +import com.artemchep.keyguard.ui.theme.combineAlpha + +@Composable +fun VaultViewQuickActionsItem( + modifier: Modifier = Modifier, + item: VaultViewItem.QuickActions, +) { + Row( + modifier = modifier + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Spacer( + modifier = Modifier + .width(4.dp), + ) + item.actions.forEach { i -> + HorizontalContextItem( + modifier = Modifier + .widthIn(max = DropdownMinWidth), + item = i, + ) + } + } +} + +@Composable +private fun HorizontalContextItem( + modifier: Modifier = Modifier, + item: ContextItem, +) = when (item) { + is ContextItem.Section -> { + Section( + modifier = modifier, + text = item.title, + ) + } + + is ContextItem.Custom -> { + Column( + modifier = modifier, + ) { + item.content() + } + } + + is FlatItemAction -> { + HorizontalFlatActionItem( + modifier = modifier, + item = item, + ) + } +} + +@Composable +private fun HorizontalFlatActionItem( + modifier: Modifier = Modifier, + item: FlatItemAction, +) { + val updatedOnClick by rememberUpdatedState(item.onClick) + val backgroundModifier = run { + val bg = Color.Transparent + val fg = MaterialTheme.colorScheme.surfaceColorAtElevationSemi(1.dp) + Modifier + .background(fg.compositeOver(bg)) + } + Row( + modifier = modifier + // Normal items have a small vertical padding, + // so add it here as well for consistency. + .padding( + vertical = 2.dp, + ) + .clip(RoundedCornerShape(16.dp)) + .then(backgroundModifier) + .clickable { + updatedOnClick?.invoke() + } + .minimumInteractiveComponentSize() + .padding( + horizontal = 8.dp, + vertical = 4.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + HorizontalFlatActionContent( + action = item, + compact = true, + ) + } +} + +@Composable +private fun RowScope.HorizontalFlatActionContent( + action: FlatItemAction, + compact: Boolean = false, +) { + CompositionLocalProvider( + LocalContentColor provides LocalContentColor.current + .let { color -> + if (action.onClick != null) { + color + } else { + color.combineAlpha(DisabledEmphasisAlpha) + } + }, + ) { + if ((action.icon != null && action.icon != Icons.Stub) || action.leading != null) { + Box( + modifier = Modifier + .size(24.dp), + ) { + if (action.icon != null) { + Icon( + imageVector = action.icon, + contentDescription = null, + ) + } + action.leading?.invoke() + } + Spacer( + modifier = Modifier + .width(16.dp), + ) + } + Column( + modifier = Modifier, + verticalArrangement = Arrangement.Center, + ) { + FlatItemTextContent( + title = { + Text( + text = textResource(action.title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + text = if (action.text != null) { + // composable + { + Text( + text = textResource(action.text), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontSize = 13.sp, + ) + } + } else { + null + }, + compact = compact, + ) + } + if (action.trailing != null) { + Spacer(Modifier.width(8.dp)) + action.trailing.invoke() + } + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt index 91bcfd15..5e95bebf 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt @@ -52,6 +52,13 @@ sealed interface VaultViewItem { companion object } + data class QuickActions( + override val id: String, + val actions: ImmutableList = persistentListOf(), + ) : VaultViewItem { + companion object + } + data class Action( override val id: String, val elevation: Dp = 0.dp, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt index accbbdec..dd1153bc 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.automirrored.outlined.ShortText import androidx.compose.material.icons.outlined.AccountBox import androidx.compose.material.icons.outlined.CopyAll import androidx.compose.material.icons.outlined.DataArray +import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.OfflineBolt import androidx.compose.material.icons.outlined.Password @@ -153,6 +154,11 @@ val onboardingItemsWatchtower = listOf( ) val onboardingItemsOther = listOf( + OnboardingItem( + title = Res.string.feat_item_export_title, + text = Res.string.feat_item_export_text, + icon = Icons.Outlined.Download, + ), OnboardingItem( title = Res.string.feat_item_multi_selection_title, text = Res.string.feat_item_multi_selection_text,