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,