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,