feat: When filtering by attachments show all of the attachments in the list

This commit is contained in:
Artem Chepurnoy 2024-02-13 17:35:48 +02:00
parent 7c2f73649d
commit f02cac1ca1
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
8 changed files with 235 additions and 118 deletions

View File

@ -298,6 +298,9 @@ fun produceDuplicatesListState(
.partially1(cipher),
)
},
onClickAttachment = {
null
},
onClickPasskey = {
null
},

View File

@ -760,6 +760,7 @@ private fun TotpTextField(
Box {
VaultViewTotpBadge2(
modifier = Modifier
.padding(top = 8.dp)
.padding(start = 16.dp),
copyText = state.copyText,
totpToken = totpToken,

View File

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.FlowRowScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@ -36,7 +37,6 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
@ -64,8 +64,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import arrow.core.partially1
import com.artemchep.keyguard.common.model.DSecret
import com.artemchep.keyguard.common.model.fileName
import com.artemchep.keyguard.common.model.fileSize
import com.artemchep.keyguard.feature.EmptyView
import com.artemchep.keyguard.feature.favicon.FaviconImage
import com.artemchep.keyguard.feature.filepicker.humanReadableByteCountSI
import com.artemchep.keyguard.feature.home.vault.model.VaultItem2
import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
import com.artemchep.keyguard.feature.localization.textResource
@ -84,7 +87,6 @@ import com.artemchep.keyguard.ui.icons.IconSmallBox
import com.artemchep.keyguard.ui.icons.KeyguardAttachment
import com.artemchep.keyguard.ui.icons.KeyguardFavourite
import com.artemchep.keyguard.ui.rightClickable
import com.artemchep.keyguard.ui.surface.LocalSurfaceColor
import com.artemchep.keyguard.ui.theme.Dimens
import com.artemchep.keyguard.ui.theme.combineAlpha
import com.artemchep.keyguard.ui.theme.isDark
@ -92,6 +94,7 @@ import com.artemchep.keyguard.ui.theme.selectedContainer
import com.artemchep.keyguard.ui.util.HorizontalDivider
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.collections.immutable.ImmutableList
import kotlin.math.ln
@Composable
@ -319,89 +322,56 @@ fun VaultListItemText(
if (item.token != null) {
VaultViewTotpBadge2(
color = badgeColor,
modifier = Modifier
.padding(top = 8.dp),
copyText = item.copyText,
totpToken = item.token,
)
}
if (item.passkeys.isNotEmpty()) {
FlowRow(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
item.passkeys.forEach {
key(it.source.credentialId) {
Surface(
color = badgeColor.takeIf { it.isSpecified }
?: MaterialTheme.colorScheme.surface,
shape = MaterialTheme.shapes.small,
tonalElevation = 1.dp,
) {
Row(
modifier = Modifier
.clickable(onClick = it.onClick)
.padding(
start = 8.dp,
end = 8.dp,
top = 8.dp,
bottom = 8.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(16.dp),
) {
IconSmallBox(
main = Icons.Outlined.Key,
)
}
Spacer(
modifier = Modifier
.width(8.dp),
)
val userDisplayName = it.source.userDisplayName
Text(
modifier = Modifier
.widthIn(max = 128.dp)
.alignByBaseline(),
text = userDisplayName
?: stringResource(Res.strings.empty_value),
color = if (userDisplayName != null) {
LocalContentColor.current
} else {
LocalContentColor.current
.combineAlpha(DisabledEmphasisAlpha)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
Spacer(
modifier = Modifier
.width(8.dp),
)
val rpId = it.source.rpId
Text(
modifier = Modifier
.widthIn(max = 128.dp)
.alignByBaseline(),
text = rpId,
color = LocalContentColor.current
.combineAlpha(MediumEmphasisAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
}
}
SmartBadgeList(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
items = item.passkeys,
key = { it.source.credentialId },
) {
SmartBadge(
modifier = Modifier,
icon = {
IconSmallBox(
main = Icons.Outlined.Key,
)
},
title = it.source.userDisplayName,
text = it.source.rpId,
onClick = it.onClick,
)
}
SmartBadgeList(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
items = item.attachments2,
key = { it.source.id },
) {
SmartBadge(
modifier = Modifier,
icon = {
IconSmallBox(
main = Icons.Outlined.KeyguardAttachment,
)
},
title = it.source.fileName(),
text = it.source.fileSize()
?.let(::humanReadableByteCountSI).orEmpty(),
// TODO: I'm not sure what we can do by clicking
// on the attachment. Would be nice to support the
// whole feature set of the attachment item at some
// point, although that would be pretty complicated.
onClick = null,
)
}
// Inject the dropdown popup to the bottom of the
@ -520,6 +490,121 @@ fun VaultListItemText(
)
}
@Composable
private inline fun <T : Any> SmartBadgeList(
modifier: Modifier = Modifier,
items: ImmutableList<T>,
crossinline key: (T) -> Any,
crossinline item: @Composable (T) -> Unit,
) {
if (items.isEmpty()) {
return
}
SmartBadgeListContainer(
modifier = modifier,
) {
items.forEach {
key(key(it)) {
item(it)
}
}
}
}
@Composable
private fun SmartBadgeListContainer(
modifier: Modifier = Modifier,
content: @Composable FlowRowScope.() -> Unit,
) {
FlowRow(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
content()
}
}
@Composable
private fun SmartBadge(
modifier: Modifier = Modifier,
icon: @Composable () -> Unit,
title: String?,
text: String?,
onClick: (() -> Unit)? = null,
) {
val updatedOnClick by rememberUpdatedState(onClick)
val backgroundModifier = if (updatedOnClick != null) {
val tintColor = MaterialTheme.colorScheme
.surfaceColorAtElevationSemi(1.dp)
Modifier
.background(tintColor)
} else {
Modifier
}
Row(
modifier = modifier
.clip(MaterialTheme.shapes.small)
.then(backgroundModifier)
.clickable(enabled = updatedOnClick != null) {
updatedOnClick?.invoke()
}
.padding(
start = 8.dp,
end = 8.dp,
top = 8.dp,
bottom = 8.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(16.dp),
) {
icon()
}
Spacer(
modifier = Modifier
.width(8.dp),
)
Text(
modifier = Modifier
.widthIn(max = 128.dp)
.alignByBaseline(),
text = title
?: stringResource(Res.strings.empty_value),
color = if (title != null) {
LocalContentColor.current
} else {
LocalContentColor.current
.combineAlpha(DisabledEmphasisAlpha)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
if (text != null) {
Spacer(
modifier = Modifier
.width(8.dp),
)
Text(
modifier = Modifier
.widthIn(max = 128.dp)
.alignByBaseline(),
text = text,
color = LocalContentColor.current
.combineAlpha(MediumEmphasisAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
private enum class Try {
CHECKBOX,
CHEVRON,

View File

@ -38,6 +38,7 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.remember
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.isSpecified
import androidx.compose.ui.text.font.FontWeight
@ -175,7 +176,6 @@ fun RowScope.VaultViewTotpBadge(
@Composable
fun VaultViewTotpBadge2(
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
copyText: CopyText,
totpToken: TotpToken,
) {
@ -183,47 +183,42 @@ fun VaultViewTotpBadge2(
totpToken = totpToken,
)
Surface(
val tintColor = MaterialTheme.colorScheme
.surfaceColorAtElevationSemi(1.dp)
Row(
modifier = modifier
.padding(top = 8.dp),
color = color.takeIf { it.isSpecified }
?: MaterialTheme.colorScheme.surface,
shape = MaterialTheme.shapes.small,
tonalElevation = 1.dp,
.clip(MaterialTheme.shapes.small)
.background(tintColor)
.clickable(enabled = state is VaultViewTotpItemBadgeState.Success) {
val code = (state as? VaultViewTotpItemBadgeState.Success)
?.codeRaw ?: return@clickable
copyText.copy(
text = code,
hidden = false,
type = CopyText.Type.OTP,
)
}
.padding(
start = 8.dp,
end = 4.dp,
top = 4.dp,
bottom = 4.dp,
),
) {
Row(
VaultViewTotpCodeContent(
totp = totpToken,
codes = (state as? VaultViewTotpItemBadgeState.Success?)
?.codes,
)
Spacer(
modifier = Modifier
.clickable(enabled = state is VaultViewTotpItemBadgeState.Success) {
val code = (state as? VaultViewTotpItemBadgeState.Success)
?.codeRaw ?: return@clickable
copyText.copy(
text = code,
hidden = false,
type = CopyText.Type.OTP,
)
}
.padding(
start = 8.dp,
end = 4.dp,
top = 4.dp,
bottom = 4.dp,
),
) {
VaultViewTotpCodeContent(
totp = totpToken,
codes = (state as? VaultViewTotpItemBadgeState.Success?)
?.codes,
)
.width(16.dp),
)
Spacer(
modifier = Modifier
.width(16.dp),
)
VaultViewTotpRemainderBadge(
state = state,
)
}
VaultViewTotpRemainderBadge(
state = state,
)
}
}

View File

@ -95,6 +95,7 @@ sealed interface VaultItem2 {
val copyText: CopyText,
val token: TotpToken?,
val passkeys: ImmutableList<Passkey>,
val attachments2: ImmutableList<Attachment>,
/**
* The name of the item.
*/
@ -138,6 +139,12 @@ sealed interface VaultItem2 {
val onClick: () -> Unit,
)
@Immutable
data class Attachment(
val source: DSecret.Attachment,
val onClick: () -> Unit,
)
@Immutable
sealed interface Feature {
@Immutable

View File

@ -40,6 +40,7 @@ suspend fun DSecret.toVaultListItem(
groupId: String? = null,
organizationsById: Map<String, DOrganization>,
onClick: (List<FlatItemAction>) -> VaultItem2.Item.Action,
onClickAttachment: suspend (DSecret.Attachment) -> (() -> Unit)?,
onClickPasskey: suspend (DSecret.Login.Fido2Credentials) -> (() -> Unit)?,
localStateFlow: StateFlow<VaultItem2.Item.LocalState>,
): VaultItem2.Item {
@ -122,6 +123,16 @@ suspend fun DSecret.toVaultListItem(
)
}
.toImmutableList(),
attachments2 = attachments
.mapNotNull {
val onClick = onClickAttachment(it)
?: return@mapNotNull null
VaultItem2.Item.Attachment(
source = it,
onClick = onClick,
)
}
.toImmutableList(),
password = DSecret.login.password.getOrNull(this),
passwordRevisionDate = DSecret.login.passwordRevisionDate.getOrNull(this),
score = DSecret.login.passwordStrength.getOrNull(this),

View File

@ -806,6 +806,12 @@ fun vaultListScreenState(
actions = dropdown,
)
},
onClickAttachment = { attachment ->
// lambda
{
// Do nothing
}
},
onClickPasskey = { credential ->
if (mode is AppMode.PickPasskey) {
val matches = passkeyTargetCheck(credential, mode.target)
@ -1213,6 +1219,10 @@ fun vaultListScreenState(
?.let {
DFilter.findOne(it, DFilter.ByOtp::class.java)
} != null
val keepAttachment = it.filterConfig?.filter
?.let {
DFilter.findOne(it, DFilter.ByAttachments::class.java)
} != null
val keepPasskey = it.filterConfig?.filter
?.let {
DFilter.findOne(it, DFilter.ByPasskeys::class.java)
@ -1232,6 +1242,8 @@ fun vaultListScreenState(
token = it.token.takeIf { keepOtp },
passkeys = it.passkeys.takeIf { keepPasskey }
?: persistentListOf(),
attachments2 = it.attachments2.takeIf { keepAttachment }
?: persistentListOf(),
)
}

View File

@ -212,6 +212,9 @@ fun vaultRecentScreenState(
onClick = cipherSink::emit.partially1(secret),
)
},
onClickAttachment = {
null
},
onClickPasskey = {
null
},