From 66d9d9bd8953966f7a0094f43aed2487973bb042 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 19:49:05 +0200 Subject: [PATCH 01/11] refactor: move UiPassword model --- .../feature/item/core/presentation/login}/model/UiPassword.kt | 2 +- .../create/presentation/password/GeneratePasswordContent.kt | 2 +- .../create/presentation/password/GeneratePasswordViewModel.kt | 2 +- .../presentation/password/model/GeneratePasswordUiState.kt | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) rename feature/item/{create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password => core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login}/model/UiPassword.kt (92%) diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/UiPassword.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt similarity index 92% rename from feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/UiPassword.kt rename to feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt index 14c97d270..be4ee5278 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/UiPassword.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.feature.item.create.presentation.password.model +package de.davis.keygo.feature.item.core.presentation.login.model data class UiPassword(val value: String) { diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt index 40f499fce..2a304e1fb 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt @@ -38,10 +38,10 @@ import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.core.ui.components.KeyGoCardProperties import de.davis.keygo.core.util.presentation.ObserveAsEvents +import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword import de.davis.keygo.feature.item.create.R import de.davis.keygo.feature.item.create.presentation.password.model.GeneratePasswordUiEvent import de.davis.keygo.feature.item.create.presentation.password.model.UiCharacterSet -import de.davis.keygo.feature.item.create.presentation.password.model.UiPassword import org.koin.androidx.compose.koinViewModel import de.davis.keygo.core.item.R as CoreItemR diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordViewModel.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordViewModel.kt index 1be84b25f..04e0aae25 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordViewModel.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordViewModel.kt @@ -6,11 +6,11 @@ import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.davis.keygo.core.item.domain.estimator.PasswordStrengthEstimator +import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword.Companion.asUiPassword import de.davis.keygo.feature.item.create.domain.PasswordGenerator import de.davis.keygo.feature.item.create.presentation.password.model.GeneratePasswordUiEvent import de.davis.keygo.feature.item.create.presentation.password.model.GeneratePasswordUiState import de.davis.keygo.feature.item.create.presentation.password.model.UiCharacterSet -import de.davis.keygo.feature.item.create.presentation.password.model.UiPassword.Companion.asUiPassword import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/GeneratePasswordUiState.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/GeneratePasswordUiState.kt index 82b065477..c958e0cdb 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/GeneratePasswordUiState.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/model/GeneratePasswordUiState.kt @@ -1,6 +1,7 @@ package de.davis.keygo.feature.item.create.presentation.password.model import de.davis.keygo.core.item.domain.model.PasswordScore +import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword internal data class GeneratePasswordUiState( val generatedPassword: UiPassword = UiPassword(""), From 148c1bc12ca565d777bb76337fe9f737d555f9df Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 20:26:07 +0200 Subject: [PATCH 02/11] feat: color password --- .../presentation/login/model/UiPassword.kt | 43 ++++++++++++++++--- .../password/GeneratePasswordContent.kt | 20 +-------- .../item/view/login/ViewLoginContent.kt | 6 ++- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt index be4ee5278..943f45eeb 100644 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt @@ -1,16 +1,26 @@ package de.davis.keygo.feature.item.core.presentation.login.model +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle + data class UiPassword(val value: String) { val parts: List = value.splitByCharClassRegex() sealed interface Part { - data class Letter(val text: String) : Part - data class Number(val text: String) : Part - data class Symbol(val text: String) : Part + val text: String + + data class Letter(override val text: String) : Part + data class Number(override val text: String) : Part + data class Symbol(override val text: String) : Part } - private fun String.splitByCharClassRegex(): List { + private fun CharSequence.splitByCharClassRegex(): List { return PATTERN.findAll(this) .map { when { @@ -22,9 +32,30 @@ data class UiPassword(val value: String) { .toList() } - internal companion object { + companion object { val PATTERN = Regex("""\p{L}+|\d+|[^\p{L}\d]+""") fun String.asUiPassword() = UiPassword(this) } -} \ No newline at end of file +} + +@Composable +fun UiPassword.colored( + numberColor: Color = MaterialTheme.colorScheme.primary, + symbolColor: Color = MaterialTheme.colorScheme.tertiary +) = remember(this, numberColor, symbolColor) { + buildAnnotatedString { + parts.forEach { + when (it) { + is UiPassword.Part.Letter -> append(it.text) + is UiPassword.Part.Number -> withStyle(SpanStyle(color = numberColor)) { + append(it.text) + } + + is UiPassword.Part.Symbol -> withStyle(SpanStyle(color = symbolColor)) { + append(it.text) + } + } + } + } +} diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt index 2a304e1fb..07af0c282 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt @@ -29,16 +29,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.core.ui.components.KeyGoCardProperties import de.davis.keygo.core.util.presentation.ObserveAsEvents -import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword +import de.davis.keygo.feature.item.core.presentation.login.model.colored import de.davis.keygo.feature.item.create.R import de.davis.keygo.feature.item.create.presentation.password.model.GeneratePasswordUiEvent import de.davis.keygo.feature.item.create.presentation.password.model.UiCharacterSet @@ -118,20 +115,7 @@ fun GeneratePasswordContent( } ) { Text( - text = buildAnnotatedString { - state.generatedPassword.parts.forEach { - when (it) { - is UiPassword.Part.Letter -> append(it.text) - is UiPassword.Part.Number -> withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append(it.text) - } - - is UiPassword.Part.Symbol -> withStyle(SpanStyle(color = MaterialTheme.colorScheme.tertiary)) { - append(it.text) - } - } - } - } + text = state.generatedPassword.colored() ) StrengthIndicator( diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index 21a554d04..4b932b46f 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -68,6 +68,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.davis.keygo.core.item.domain.alias.newItemId @@ -81,6 +82,8 @@ import de.davis.keygo.feature.item.core.presentation.component.CopyToClipboardBu import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField import de.davis.keygo.feature.item.core.presentation.login.model.FieldType +import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword.Companion.asUiPassword +import de.davis.keygo.feature.item.core.presentation.login.model.colored import de.davis.keygo.feature.item.core.presentation.transformation.TrimTransformation import de.davis.keygo.feature.item.core.presentation.transformation.rememberSchemeStrippingTransformation import de.davis.keygo.feature.item.view.R @@ -238,7 +241,8 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) ) { val scrollState = rememberScrollState() Text( - text = if (isPasswordHidden) pwd.hidden else pwd.raw, + text = if (isPasswordHidden) AnnotatedString(pwd.hidden) + else pwd.raw.asUiPassword().colored(), maxLines = 1, modifier = Modifier.horizontalScroll(scrollState), ) From e600675c5f31856faed4e2c92d6c98c210f70087 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 20:42:40 +0200 Subject: [PATCH 03/11] feat: change font family so letters are distinguishable --- .../item/core/presentation/component/KeyGoFormField.kt | 7 ++++++- .../feature/item/create/presentation/login/LoginContent.kt | 3 +++ .../presentation/password/GeneratePasswordContent.kt | 4 +++- .../keygo/feature/item/view/login/ViewLoginContent.kt | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/KeyGoFormField.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/KeyGoFormField.kt index fbf6b55d3..8f9acd79a 100644 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/KeyGoFormField.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/KeyGoFormField.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextObfuscationMode import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedSecureTextField import androidx.compose.material3.OutlinedTextField @@ -28,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -41,6 +43,7 @@ import de.davis.keygo.feature.item.core.presentation.transformation.TrimTransfor fun KeyGoFormField( state: TextFieldState, modifier: Modifier = Modifier, + textStyle: TextStyle = LocalTextStyle.current, isSecure: Boolean = false, label: @Composable (TextFieldLabelScope.() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, @@ -53,7 +56,7 @@ fun KeyGoFormField( onKeyboardAction: KeyboardActionHandler? = null, inputTransformation: InputTransformation? = TrimTransformation, outputTransformation: OutputTransformation? = null, - interactionSource: MutableInteractionSource? = null + interactionSource: MutableInteractionSource? = null, ) { val supportingText: @Composable (() -> Unit)? = error?.let { { @@ -79,6 +82,7 @@ fun KeyGoFormField( modifier = Modifier .weight(1f) .trimOnFocusLost(state, inputTransformation is TrimTransformation), + textStyle = textStyle, label = label, placeholder = placeholder, prefix = prefix, @@ -107,6 +111,7 @@ fun KeyGoFormField( modifier = Modifier .weight(1f) .trimOnFocusLost(state, inputTransformation is TrimTransformation), + textStyle = textStyle, label = label, placeholder = placeholder, prefix = prefix, diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt index 8547f4fa9..f6738ebea 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -33,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -205,6 +207,7 @@ private fun LoginReadyContent( modifier = Modifier.onFocusChanged { forceCompact = !it.hasFocus }, + textStyle = LocalTextStyle.current.merge(fontFamily = FontFamily.Monospace), placeholder = { Text(text = stringResource(CoreItemR.string.password)) }, isSecure = true, outsideTrailingContent = { diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt index 07af0c282..3385c653c 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.davis.keygo.core.item.presentation.StrengthIndicator @@ -115,7 +116,8 @@ fun GeneratePasswordContent( } ) { Text( - text = state.generatedPassword.colored() + text = state.generatedPassword.colored(), + fontFamily = FontFamily.Monospace, ) StrengthIndicator( diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index 4b932b46f..df258efc8 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.davis.keygo.core.item.domain.alias.newItemId @@ -243,6 +244,7 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) Text( text = if (isPasswordHidden) AnnotatedString(pwd.hidden) else pwd.raw.asUiPassword().colored(), + fontFamily = FontFamily.Monospace, maxLines = 1, modifier = Modifier.horizontalScroll(scrollState), ) From f7b19276bd078e9f1d4bd2f49a3a33da488c2e1d Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 21:34:53 +0200 Subject: [PATCH 04/11] refactor: centralize entry function --- .../item/core/presentation/LazyListEntry.kt | 35 +++++++++++++++++++ .../view/creditcard/ViewCreditCardContent.kt | 29 +-------------- .../item/view/login/ViewLoginContent.kt | 29 +-------------- 3 files changed, 37 insertions(+), 56 deletions(-) create mode 100644 feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt new file mode 100644 index 000000000..97cf89a09 --- /dev/null +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt @@ -0,0 +1,35 @@ +package de.davis.keygo.feature.item.core.presentation + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import de.davis.keygo.core.ui.components.KeyGoCard + +fun LazyListScope.entry( + title: String, + leadingIcon: ImageVector, + modifier: Modifier = Modifier, + trailingContent: @Composable (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + item(key = title) { + KeyGoCard( + title = { + Text(text = title) + }, + leadingItem = { + Icon( + imageVector = leadingIcon, + contentDescription = null, + ) + }, + trailingItem = trailingContent, + modifier = modifier.animateItem(), + ) { + content() + } + } +} diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt index b5e40dde5..30d4e3d2f 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -59,11 +58,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.davis.keygo.core.item.presentation.toImageVector -import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode import de.davis.keygo.feature.item.core.presentation.component.CopyToClipboardButton import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField +import de.davis.keygo.feature.item.core.presentation.entry import de.davis.keygo.feature.item.core.presentation.transformation.TrimTransformation import de.davis.keygo.feature.item.view.R import de.davis.keygo.feature.item.view.creditcard.model.CreditCardFieldType @@ -406,32 +405,6 @@ private fun CreditCardFieldType.addIcon(): ImageVector { } } -private fun LazyListScope.entry( - title: String, - leadingIcon: ImageVector, - modifier: Modifier = Modifier, - trailingContent: @Composable (() -> Unit)? = null, - content: @Composable () -> Unit, -) { - item(key = title) { - KeyGoCard( - title = { - Text(text = title) - }, - leadingItem = { - Icon( - imageVector = leadingIcon, - contentDescription = null, - ) - }, - trailingItem = trailingContent, - modifier = modifier.animateItem(), - ) { - content() - } - } -} - @Preview @Composable private fun ViewCreditCardContentPreview() { diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index df258efc8..f58211dff 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -77,11 +76,11 @@ import de.davis.keygo.core.item.domain.model.DomainInfo import de.davis.keygo.core.item.domain.model.PasswordScore import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.item.presentation.toImageVector -import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode import de.davis.keygo.feature.item.core.presentation.component.CopyToClipboardButton import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField +import de.davis.keygo.feature.item.core.presentation.entry import de.davis.keygo.feature.item.core.presentation.login.model.FieldType import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword.Companion.asUiPassword import de.davis.keygo.feature.item.core.presentation.login.model.colored @@ -527,32 +526,6 @@ private fun FieldType.addIcon(): ImageVector { } } -private fun LazyListScope.entry( - title: String, - leadingIcon: ImageVector, - modifier: Modifier = Modifier, - trailingContent: @Composable (() -> Unit)? = null, - content: @Composable () -> Unit, -) { - item(key = title) { - KeyGoCard( - title = { - Text(text = title) - }, - leadingItem = { - Icon( - imageVector = leadingIcon, - contentDescription = null, - ) - }, - trailingItem = trailingContent, - modifier = modifier.animateItem(), - ) { - content() - } - } -} - @Preview @Composable private fun ViewLoginContentPreview() { From 8b8f5a3b7beb68a2709b1d381215b1d398c38b96 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 22:21:59 +0200 Subject: [PATCH 05/11] feat: migrate to copy entry --- .../item/core/presentation/LazyListEntry.kt | 68 +++++++++++++++++++ .../item/core/src/main/res/values/strings.xml | 2 + .../view/creditcard/ViewCreditCardContent.kt | 34 ++++++---- .../item/view/login/ViewLoginContent.kt | 28 ++++---- .../davis/keygo/feature/item/view/onHold.kt | 29 -------- 5 files changed, 105 insertions(+), 56 deletions(-) delete mode 100644 feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/onHold.kt diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt index 97cf89a09..f0ecc50d3 100644 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt @@ -1,12 +1,25 @@ package de.davis.keygo.feature.item.core.presentation +import android.content.ClipData +import android.content.ClipDescription +import android.os.Build +import android.os.PersistableBundle +import android.widget.Toast +import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.platform.toClipEntry import de.davis.keygo.core.ui.components.KeyGoCard +import de.davis.keygo.feature.item.core.R +import kotlinx.coroutines.launch fun LazyListScope.entry( title: String, @@ -33,3 +46,58 @@ fun LazyListScope.entry( } } } + +fun LazyListScope.copyableEntry( + title: String, + leadingIcon: ImageVector, + dataToCopy: () -> String, + sensitive: Boolean = false, + modifier: Modifier = Modifier, + trailingContent: @Composable (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + item(key = title) { + val scope = rememberCoroutineScope() + val clipboard = LocalClipboard.current + val context = LocalContext.current + val resources = LocalResources.current + + KeyGoCard( + title = { + Text(text = title) + }, + leadingItem = { + Icon( + imageVector = leadingIcon, + contentDescription = null, + ) + }, + trailingItem = trailingContent, + modifier = modifier + .animateItem() + .clickable { + val data = dataToCopy() + val clipData = ClipData.newPlainText(data, data).apply { + if (sensitive) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + description.extras = PersistableBundle().apply { + putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) + } + } + } + scope.launch { + clipboard.setClipEntry(clipData.toClipEntry()) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) + Toast.makeText( + context, + resources.getString(R.string.copied, title), + Toast.LENGTH_SHORT + ).show() + } + }, + ) { + content() + } + } +} diff --git a/feature/item/core/src/main/res/values/strings.xml b/feature/item/core/src/main/res/values/strings.xml index a2ce8e831..fd951b4a1 100644 --- a/feature/item/core/src/main/res/values/strings.xml +++ b/feature/item/core/src/main/res/values/strings.xml @@ -27,4 +27,6 @@ This field can not be blank This input is invalid Something went wrong. Please try again. + + %s copied diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt index 30d4e3d2f..e5bb88a17 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt @@ -58,10 +58,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.davis.keygo.core.item.presentation.toImageVector +import de.davis.keygo.core.ui.components.VisibilityButton import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode -import de.davis.keygo.feature.item.core.presentation.component.CopyToClipboardButton import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField +import de.davis.keygo.feature.item.core.presentation.copyableEntry import de.davis.keygo.feature.item.core.presentation.entry import de.davis.keygo.feature.item.core.presentation.transformation.TrimTransformation import de.davis.keygo.feature.item.view.R @@ -69,7 +70,6 @@ import de.davis.keygo.feature.item.view.creditcard.model.CreditCardFieldType import de.davis.keygo.feature.item.view.creditcard.model.ViewCreditCardState import de.davis.keygo.feature.item.view.creditcard.model.ViewCreditCardUiEvent import de.davis.keygo.feature.item.view.login.model.ObfuscatedString -import de.davis.keygo.feature.item.view.onHold import de.davis.keygo.core.item.R as CoreItemR import de.davis.keygo.core.ui.R as CoreUiR import de.davis.keygo.feature.item.core.R as ItemCoreR @@ -171,9 +171,10 @@ fun ViewCreditCardContent(state: ViewCreditCardState, onEvent: (ViewCreditCardUi } if (state.holder.isNotBlank()) { - entry( + copyableEntry( title = cardholder, leadingIcon = Icons.Default.Person, + dataToCopy = { state.holder }, ) { Text(text = state.holder) } @@ -181,14 +182,16 @@ fun ViewCreditCardContent(state: ViewCreditCardState, onEvent: (ViewCreditCardUi val cardNum = state.cardNumber if (cardNum != null) { - entry( + copyableEntry( title = cardNumber, leadingIcon = Icons.Default.CreditCard, - modifier = Modifier.onHold { - isCardNumberHidden = !it - }, + dataToCopy = { cardNum.raw }, + sensitive = true, trailingContent = { - CopyToClipboardButton(cardNum.raw) + VisibilityButton( + isHidden = isCardNumberHidden, + onClick = { isCardNumberHidden = !isCardNumberHidden } + ) }, ) { val scrollState = rememberScrollState() @@ -202,14 +205,16 @@ fun ViewCreditCardContent(state: ViewCreditCardState, onEvent: (ViewCreditCardUi val cvvVal = state.cvv if (cvvVal != null) { - entry( + copyableEntry( title = cvv, leadingIcon = Icons.Default.Pin, - modifier = Modifier.onHold { - isCvvHidden = !it - }, + dataToCopy = { cvvVal.raw }, + sensitive = true, trailingContent = { - CopyToClipboardButton(cvvVal.raw) + VisibilityButton( + isHidden = isCvvHidden, + onClick = { isCvvHidden = !isCvvHidden } + ) }, ) { val scrollState = rememberScrollState() @@ -222,9 +227,10 @@ fun ViewCreditCardContent(state: ViewCreditCardState, onEvent: (ViewCreditCardUi } if (state.expirationDate.isNotBlank()) { - entry( + copyableEntry( title = expiration, leadingIcon = Icons.Default.CalendarMonth, + dataToCopy = { state.expirationDate }, ) { Text(text = state.expirationDate) } diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index f58211dff..3b5dfd1b3 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -76,10 +76,11 @@ import de.davis.keygo.core.item.domain.model.DomainInfo import de.davis.keygo.core.item.domain.model.PasswordScore import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.item.presentation.toImageVector +import de.davis.keygo.core.ui.components.VisibilityButton import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode -import de.davis.keygo.feature.item.core.presentation.component.CopyToClipboardButton import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField +import de.davis.keygo.feature.item.core.presentation.copyableEntry import de.davis.keygo.feature.item.core.presentation.entry import de.davis.keygo.feature.item.core.presentation.login.model.FieldType import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword.Companion.asUiPassword @@ -92,7 +93,6 @@ import de.davis.keygo.feature.item.view.login.model.ObfuscatedString import de.davis.keygo.feature.item.view.login.model.TotpState import de.davis.keygo.feature.item.view.login.model.ViewLoginState import de.davis.keygo.feature.item.view.login.model.ViewLoginUiEvent -import de.davis.keygo.feature.item.view.onHold import de.davis.keygo.feature.totp.domain.model.TotpValue import de.davis.keygo.feature.totp.presentation.component.QRScanner import de.davis.keygo.feature.totp.presentation.component.TotpParseErrorDialog @@ -229,15 +229,17 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) val pwd = state.password val score = state.passwordStrengthScore if (pwd != null && score != null) { - entry( + copyableEntry( title = password, leadingIcon = Icons.Default.Password, - modifier = Modifier.onHold { - isPasswordHidden = !it - }, + dataToCopy = { pwd.raw }, + sensitive = true, trailingContent = { - CopyToClipboardButton(pwd.raw) - }, + VisibilityButton( + isHidden = isPasswordHidden, + onClick = { isPasswordHidden = !isPasswordHidden } + ) + } ) { val scrollState = rememberScrollState() Text( @@ -255,12 +257,11 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) } when (totpState) { - is TotpState.HasTotp -> entry( + is TotpState.HasTotp -> copyableEntry( title = totp, leadingIcon = Icons.Default.AccessTime, - trailingContent = { - CopyToClipboardButton(state.totpState.value.code) - }, + dataToCopy = { state.totpState.value.code }, + sensitive = true, ) { Text(text = state.totpState.formattedCode) LinearProgressIndicator( @@ -280,9 +281,10 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) } if (state.username.isNotBlank()) { - entry( + copyableEntry( title = username, leadingIcon = Icons.Default.Person, + dataToCopy = { state.username }, ) { Text(text = state.username) } diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/onHold.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/onHold.kt deleted file mode 100644 index b654b21c7..000000000 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/onHold.kt +++ /dev/null @@ -1,29 +0,0 @@ -package de.davis.keygo.feature.item.view - -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed -import androidx.compose.ui.input.pointer.pointerInput - -fun Modifier.onHold(onHold: (Boolean) -> Unit) = this.pointerInput(Unit) { - awaitEachGesture { - val down = awaitFirstDown() - onHold(true) - - try { - val pointerId = down.id - do { - val event = awaitPointerEvent() - val change = event.changes.firstOrNull { it.id == pointerId } - if (change == null || change.changedToUpIgnoreConsumed()) { - break - } - - change.consume() - } while (true) - } finally { - onHold(false) - } - } -} \ No newline at end of file From 7b4ab4280411fd5f9e7645c76e933063713e8382 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 22:29:09 +0200 Subject: [PATCH 06/11] feat: introduce clickable KeyGoCard --- .../keygo/core/ui/components/KeyGoCard.kt | 106 +++++++++++++----- .../credit_card/presentation/CardScanEntry.kt | 9 +- .../item/core/presentation/LazyListEntry.kt | 46 ++++---- 3 files changed, 99 insertions(+), 62 deletions(-) diff --git a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt index 8e2b192eb..c9277078f 100644 --- a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt +++ b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt @@ -68,40 +68,84 @@ fun KeyGoCard( elevation = elevation, border = border, ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - leadingItem?.let { - Box(modifier = Modifier.minimumInteractiveComponentSize()) { - leadingItem() - } - } + KeyGoCardContent( + title = title, + leadingItem = leadingItem, + trailingItem = trailingItem, + content = content + ) + } + } +} - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodySmall - ) { - title() - } +@Composable +fun KeyGoCard( + onClick: () -> Unit, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + properties: KeyGoCardProperties = KeyGoCardProperties.outlined(), + leadingItem: @Composable (() -> Unit)? = null, + trailingItem: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + with(properties) { + Card( + onClick = onClick, + modifier = modifier, + shape = shape, + colors = colors, + elevation = elevation, + border = border, + ) { + KeyGoCardContent( + title = title, + leadingItem = leadingItem, + trailingItem = trailingItem, + content = content + ) + } + } +} - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodyLarge - ) { - content() - } - } +@Composable +private fun KeyGoCardContent( + title: @Composable () -> Unit, + leadingItem: @Composable (() -> Unit)?, + trailingItem: @Composable (() -> Unit)?, + content: @Composable ColumnScope.() -> Unit, +) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + leadingItem?.let { + Box(modifier = Modifier.minimumInteractiveComponentSize()) { + leadingItem() + } + } - trailingItem?.let { - Box(modifier = Modifier.minimumInteractiveComponentSize()) { - trailingItem() - } - } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodySmall + ) { + title() + } + + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyLarge + ) { + content() + } + } + + trailingItem?.let { + Box(modifier = Modifier.minimumInteractiveComponentSize()) { + trailingItem() } } } -} \ No newline at end of file +} diff --git a/feature/credit-card/src/main/kotlin/de/davis/keygo/feature/credit_card/presentation/CardScanEntry.kt b/feature/credit-card/src/main/kotlin/de/davis/keygo/feature/credit_card/presentation/CardScanEntry.kt index 3c5fd4997..03b338e75 100644 --- a/feature/credit-card/src/main/kotlin/de/davis/keygo/feature/credit_card/presentation/CardScanEntry.kt +++ b/feature/credit-card/src/main/kotlin/de/davis/keygo/feature/credit_card/presentation/CardScanEntry.kt @@ -1,12 +1,10 @@ package de.davis.keygo.feature.credit_card.presentation -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Contactless -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -17,7 +15,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -51,10 +48,8 @@ private fun ScanCardPrompt( modifier: Modifier = Modifier, ) { KeyGoCard( - modifier = modifier - .fillMaxWidth() - .clip(CardDefaults.elevatedShape) - .clickable(onClick = onClick), + onClick = onClick, + modifier = modifier.fillMaxWidth(), properties = KeyGoCardProperties.elevated(), leadingItem = { Icon( diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt index f0ecc50d3..13634eed7 100644 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt @@ -5,7 +5,6 @@ import android.content.ClipDescription import android.os.Build import android.os.PersistableBundle import android.widget.Toast -import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -63,6 +62,27 @@ fun LazyListScope.copyableEntry( val resources = LocalResources.current KeyGoCard( + onClick = { + val data = dataToCopy() + val clipData = ClipData.newPlainText(data, data).apply { + if (sensitive) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + description.extras = PersistableBundle().apply { + putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) + } + } + } + scope.launch { + clipboard.setClipEntry(clipData.toClipEntry()) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) + Toast.makeText( + context, + resources.getString(R.string.copied, title), + Toast.LENGTH_SHORT + ).show() + } + }, title = { Text(text = title) }, @@ -73,29 +93,7 @@ fun LazyListScope.copyableEntry( ) }, trailingItem = trailingContent, - modifier = modifier - .animateItem() - .clickable { - val data = dataToCopy() - val clipData = ClipData.newPlainText(data, data).apply { - if (sensitive) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - description.extras = PersistableBundle().apply { - putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) - } - } - } - scope.launch { - clipboard.setClipEntry(clipData.toClipEntry()) - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) - Toast.makeText( - context, - resources.getString(R.string.copied, title), - Toast.LENGTH_SHORT - ).show() - } - }, + modifier = modifier.animateItem(), ) { content() } From 70300007d69d9b65b63debf664f58e91cdfe0348 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 27 Aug 2026 22:30:08 +0200 Subject: [PATCH 07/11] refactor: delete dead code --- .../component/CopyToClipboardButton.kt | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/CopyToClipboardButton.kt diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/CopyToClipboardButton.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/CopyToClipboardButton.kt deleted file mode 100644 index 86ba0b187..000000000 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/component/CopyToClipboardButton.kt +++ /dev/null @@ -1,44 +0,0 @@ -package de.davis.keygo.feature.item.core.presentation.component - -import android.content.ClipData -import android.content.ClipDescription -import android.os.Build -import android.os.PersistableBundle -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.toClipEntry -import androidx.compose.ui.res.stringResource -import de.davis.keygo.core.ui.R -import kotlinx.coroutines.launch - -@Composable -fun CopyToClipboardButton(data: String) { - val scope = rememberCoroutineScope() - val clipboard = LocalClipboard.current - - IconButton( - onClick = { - val clipData = - ClipData.newPlainText(data, data) - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - description.extras = PersistableBundle().apply { - putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) - } - } - scope.launch { - clipboard.setClipEntry(clipData.toClipEntry()) - } - }, - ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = stringResource(R.string.copy_to_clipboard_content_description) - ) - } -} \ No newline at end of file From b82c4598be139edb445b986c03d75cfac7bb63bd Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 28 Aug 2026 01:20:51 +0200 Subject: [PATCH 08/11] feat: add accessible click label to copyable entries --- app/src/main/res/values/strings.xml | 1 - .../keygo/core/ui/clipboard/Clipboard.kt | 19 +++ .../keygo/core/ui/components/KeyGoCard.kt | 12 +- core/ui/src/main/res/values/strings.xml | 1 - .../presentation/activity/AutofillActivity.kt | 22 ++-- .../item/core/presentation/LazyListEntry.kt | 109 ++++++++++-------- .../item/core/src/main/res/values/strings.xml | 1 + 7 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 core/ui/src/main/kotlin/de/davis/keygo/core/ui/clipboard/Clipboard.kt diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aeea32e40..c9855c7b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,7 +4,6 @@ Add Element Search Open website - Copy to clipboard Add new Element diff --git a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/clipboard/Clipboard.kt b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/clipboard/Clipboard.kt new file mode 100644 index 000000000..fbd895a6a --- /dev/null +++ b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/clipboard/Clipboard.kt @@ -0,0 +1,19 @@ +package de.davis.keygo.core.ui.clipboard + +import android.content.ClipData +import android.content.ClipDescription +import android.os.Build +import android.os.PersistableBundle +import androidx.compose.ui.platform.Clipboard +import androidx.compose.ui.platform.toClipEntry + +suspend fun Clipboard.setText(label: String, text: String, sensitive: Boolean = false) { + val clipData = ClipData.newPlainText(label, text).apply { + if (sensitive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + description.extras = PersistableBundle().apply { + putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) + } + } + + setClipEntry(clipData.toClipEntry()) +} diff --git a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt index c9277078f..661ab5421 100644 --- a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt +++ b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt @@ -21,7 +21,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.compose.ui.semantics.onClick as onClickAction @Immutable data class KeyGoCardProperties( @@ -83,15 +85,23 @@ fun KeyGoCard( onClick: () -> Unit, title: @Composable () -> Unit, modifier: Modifier = Modifier, + onClickLabel: String? = null, properties: KeyGoCardProperties = KeyGoCardProperties.outlined(), leadingItem: @Composable (() -> Unit)? = null, trailingItem: @Composable (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) { + // Semantics apply innermost first, so Card's own clickable has already written its click + // action with a null label by the time this runs. Setting an accessibility action merges field + // by field, so a null action here keeps that click, which is what carries the enabled state. + val labelled = + if (onClickLabel == null) modifier + else modifier.semantics { onClickAction(label = onClickLabel, action = null) } + with(properties) { Card( onClick = onClick, - modifier = modifier, + modifier = labelled, shape = shape, colors = colors, elevation = elevation, diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index cd5acd76b..84cb42e63 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -6,7 +6,6 @@ No matches found - Copy to clipboard Show password Hide password diff --git a/feature/autofill/src/main/kotlin/de/davis/keygo/feature/autofill/presentation/activity/AutofillActivity.kt b/feature/autofill/src/main/kotlin/de/davis/keygo/feature/autofill/presentation/activity/AutofillActivity.kt index 8304b6992..5aa85be33 100644 --- a/feature/autofill/src/main/kotlin/de/davis/keygo/feature/autofill/presentation/activity/AutofillActivity.kt +++ b/feature/autofill/src/main/kotlin/de/davis/keygo/feature/autofill/presentation/activity/AutofillActivity.kt @@ -1,12 +1,8 @@ package de.davis.keygo.feature.autofill.presentation.activity -import android.content.ClipData -import android.content.ClipDescription import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle -import android.os.PersistableBundle import android.service.autofill.Dataset import android.view.autofill.AutofillManager import androidx.activity.compose.setContent @@ -14,7 +10,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.toClipEntry +import androidx.compose.ui.res.stringResource import androidx.fragment.app.FragmentActivity import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController @@ -22,6 +18,7 @@ import de.davis.keygo.core.identity.presentation.rememberBiometricUnlockAdapter import de.davis.keygo.core.identity.presentation.useAdapter import de.davis.keygo.core.security.domain.model.BiometricPolicy import de.davis.keygo.core.security.presentation.rememberBiometricCryptoController +import de.davis.keygo.core.ui.clipboard.setText import de.davis.keygo.core.ui.theme.KeyGoTheme import de.davis.keygo.core.util.onFailure import de.davis.keygo.core.util.onSuccess @@ -37,6 +34,7 @@ import de.davis.keygo.feature.autofill.presentation.model.Request import de.davis.keygo.feature.autofill.presentation.model.RequestData import de.davis.keygo.feature.item.create.presentation.password.GeneratePasswordModalBottomSheet import org.koin.androidx.compose.koinViewModel +import de.davis.keygo.core.item.R as CoreItemR /** @@ -67,20 +65,18 @@ internal class AutofillActivity : FragmentActivity() { val biometricUnlockAdapter = rememberBiometricUnlockAdapter() val clipboard = LocalClipboard.current + val passwordLabel = stringResource(CoreItemR.string.password) ObserveAsEvents(viewModel.events) { event -> when (event) { AutofillEvent.Abort -> cancel() is AutofillEvent.Fill -> { event.copyToClipboard?.let { - val clipData = ClipData.newPlainText(it, it).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - description.extras = PersistableBundle().apply { - putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) - } - } - - clipboard.setClipEntry(clipData.toClipEntry()) + clipboard.setText( + label = passwordLabel, + text = it, + sensitive = true, + ) } finishWithResult(event.dataset) diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt index 13634eed7..cd56d7e31 100644 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/LazyListEntry.kt @@ -1,9 +1,6 @@ package de.davis.keygo.feature.item.core.presentation -import android.content.ClipData -import android.content.ClipDescription import android.os.Build -import android.os.PersistableBundle import android.widget.Toast import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon @@ -14,8 +11,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalResources -import androidx.compose.ui.platform.toClipEntry +import androidx.compose.ui.res.stringResource +import de.davis.keygo.core.ui.clipboard.setText import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.feature.item.core.R import kotlinx.coroutines.launch @@ -28,21 +25,15 @@ fun LazyListScope.entry( content: @Composable () -> Unit, ) { item(key = title) { - KeyGoCard( - title = { - Text(text = title) - }, - leadingItem = { - Icon( - imageVector = leadingIcon, - contentDescription = null, - ) - }, - trailingItem = trailingContent, + EntryCard( + title = title, + leadingIcon = leadingIcon, modifier = modifier.animateItem(), - ) { - content() - } + trailingContent = trailingContent, + onClick = null, + onClickLabel = null, + content = content, + ) } } @@ -59,43 +50,67 @@ fun LazyListScope.copyableEntry( val scope = rememberCoroutineScope() val clipboard = LocalClipboard.current val context = LocalContext.current - val resources = LocalResources.current + val copiedMessage = stringResource(R.string.copied, title) - KeyGoCard( + EntryCard( + title = title, + leadingIcon = leadingIcon, + modifier = modifier.animateItem(), + trailingContent = trailingContent, onClick = { - val data = dataToCopy() - val clipData = ClipData.newPlainText(data, data).apply { - if (sensitive) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - description.extras = PersistableBundle().apply { - putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) - } - } - } scope.launch { - clipboard.setClipEntry(clipData.toClipEntry()) + clipboard.setText( + label = title, + text = dataToCopy(), + sensitive = sensitive, + ) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) - Toast.makeText( - context, - resources.getString(R.string.copied, title), - Toast.LENGTH_SHORT - ).show() + Toast.makeText(context, copiedMessage, Toast.LENGTH_SHORT).show() } }, - title = { - Text(text = title) - }, - leadingItem = { - Icon( - imageVector = leadingIcon, - contentDescription = null, - ) - }, + onClickLabel = stringResource(R.string.copy_entry, title), + content = content, + ) + } +} + +@Composable +private fun EntryCard( + title: String, + leadingIcon: ImageVector, + modifier: Modifier, + trailingContent: @Composable (() -> Unit)?, + onClick: (() -> Unit)?, + onClickLabel: String?, + content: @Composable () -> Unit, +) { + val cardTitle: @Composable () -> Unit = { Text(text = title) } + val cardLeadingItem: @Composable () -> Unit = { + Icon( + imageVector = leadingIcon, + contentDescription = null, + ) + } + + if (onClick == null) + KeyGoCard( + title = cardTitle, + modifier = modifier, + leadingItem = cardLeadingItem, + trailingItem = trailingContent, + ) { + content() + } + else + KeyGoCard( + onClick = onClick, + title = cardTitle, + modifier = modifier, + onClickLabel = onClickLabel, + leadingItem = cardLeadingItem, trailingItem = trailingContent, - modifier = modifier.animateItem(), ) { content() } - } } diff --git a/feature/item/core/src/main/res/values/strings.xml b/feature/item/core/src/main/res/values/strings.xml index fd951b4a1..f416b33bf 100644 --- a/feature/item/core/src/main/res/values/strings.xml +++ b/feature/item/core/src/main/res/values/strings.xml @@ -29,4 +29,5 @@ Something went wrong. Please try again. %s copied + Copy %s From 6b1970c0d06248a070fd745c6b2797770eb15e60 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 28 Aug 2026 01:21:12 +0200 Subject: [PATCH 09/11] feat: apply shared secret text style to passwords and card fields --- .../davis/keygo/core/ui/theme/Typography.kt | 25 ++++++++++++++++++- .../create/presentation/login/LoginContent.kt | 5 ++-- .../password/GeneratePasswordContent.kt | 4 +-- .../view/creditcard/ViewCreditCardContent.kt | 3 +++ .../item/view/login/ViewLoginContent.kt | 4 +-- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/theme/Typography.kt b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/theme/Typography.kt index ac78cb882..92d8f9546 100644 --- a/core/ui/src/main/kotlin/de/davis/keygo/core/ui/theme/Typography.kt +++ b/core/ui/src/main/kotlin/de/davis/keygo/core/ui/theme/Typography.kt @@ -1,5 +1,28 @@ package de.davis.keygo.core.ui.theme +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily -val KeyGoTypography = Typography() \ No newline at end of file +val KeyGoTypography = Typography() + +private val Secret = TextStyle( + fontFamily = FontFamily.Monospace, + fontFeatureSettings = "tnum", +) + +/** + * The ambient text style with secret typography applied: monospace so lookalike characters stay + * apart, tabular figures so digits keep their column while a value scrolls. + * + * Use it for anything the user reads character by character, such as passwords, card numbers, CVVs + * and TOTP codes. + * + * [FontFamily.Monospace] resolves to whatever font the device ships under that alias. That is + * usually Roboto Mono, but it varies by OEM and none of them guarantee a slashed zero. Bundling a + * font to make that deterministic is a change to [Secret] alone. + */ +val secretTextStyle: TextStyle + @Composable get() = LocalTextStyle.current.merge(Secret) diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt index f6738ebea..89af65ad1 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/login/LoginContent.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -34,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -49,6 +47,7 @@ import de.davis.keygo.core.item.domain.model.VaultMetadata import de.davis.keygo.core.item.generated.domain.model.VaultItemType import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.ui.theme.KeyGoTheme +import de.davis.keygo.core.ui.theme.secretTextStyle import de.davis.keygo.feature.item.core.presentation.component.ChipFormGroup import de.davis.keygo.feature.item.core.presentation.component.CreateOrModifyItemTopAppBar import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField @@ -207,7 +206,7 @@ private fun LoginReadyContent( modifier = Modifier.onFocusChanged { forceCompact = !it.hasFocus }, - textStyle = LocalTextStyle.current.merge(fontFamily = FontFamily.Monospace), + textStyle = secretTextStyle, placeholder = { Text(text = stringResource(CoreItemR.string.password)) }, isSecure = true, outsideTrailingContent = { diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt index 3385c653c..5f63da3f3 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt @@ -29,12 +29,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.core.ui.components.KeyGoCardProperties +import de.davis.keygo.core.ui.theme.secretTextStyle import de.davis.keygo.core.util.presentation.ObserveAsEvents import de.davis.keygo.feature.item.core.presentation.login.model.colored import de.davis.keygo.feature.item.create.R @@ -117,7 +117,7 @@ fun GeneratePasswordContent( ) { Text( text = state.generatedPassword.colored(), - fontFamily = FontFamily.Monospace, + style = secretTextStyle, ) StrengthIndicator( diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt index e5bb88a17..7875b9975 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.unit.dp import de.davis.keygo.core.item.presentation.toImageVector import de.davis.keygo.core.ui.components.VisibilityButton import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode +import de.davis.keygo.core.ui.theme.secretTextStyle import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField import de.davis.keygo.feature.item.core.presentation.copyableEntry @@ -197,6 +198,7 @@ fun ViewCreditCardContent(state: ViewCreditCardState, onEvent: (ViewCreditCardUi val scrollState = rememberScrollState() Text( text = if (isCardNumberHidden) cardNum.hidden else cardNum.formatted, + style = secretTextStyle, maxLines = 1, modifier = Modifier.horizontalScroll(scrollState), ) @@ -220,6 +222,7 @@ fun ViewCreditCardContent(state: ViewCreditCardState, onEvent: (ViewCreditCardUi val scrollState = rememberScrollState() Text( text = if (isCvvHidden) cvvVal.hidden else cvvVal.raw, + style = secretTextStyle, maxLines = 1, modifier = Modifier.horizontalScroll(scrollState), ) diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index 3b5dfd1b3..605df477e 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -68,7 +68,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.davis.keygo.core.item.domain.alias.newItemId @@ -78,6 +77,7 @@ import de.davis.keygo.core.item.presentation.StrengthIndicator import de.davis.keygo.core.item.presentation.toImageVector import de.davis.keygo.core.ui.components.VisibilityButton import de.davis.keygo.core.ui.composition.LocalIsInSinglePaneMode +import de.davis.keygo.core.ui.theme.secretTextStyle import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField import de.davis.keygo.feature.item.core.presentation.copyableEntry @@ -245,7 +245,7 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) Text( text = if (isPasswordHidden) AnnotatedString(pwd.hidden) else pwd.raw.asUiPassword().colored(), - fontFamily = FontFamily.Monospace, + style = secretTextStyle, maxLines = 1, modifier = Modifier.horizontalScroll(scrollState), ) From dc1176ff03b2c4d1e2a5dc21eb47f4b48c3619a8 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 28 Aug 2026 01:21:30 +0200 Subject: [PATCH 10/11] refactor: move UiPassword.colored() out of the model package --- .../presentation/login/ColoredPassword.kt | 32 +++++++++++++++++++ .../presentation/login/model/UiPassword.kt | 32 ++----------------- .../password/GeneratePasswordContent.kt | 2 +- .../item/view/login/ViewLoginContent.kt | 5 +-- 4 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/ColoredPassword.kt diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/ColoredPassword.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/ColoredPassword.kt new file mode 100644 index 000000000..cf04ff405 --- /dev/null +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/ColoredPassword.kt @@ -0,0 +1,32 @@ +package de.davis.keygo.feature.item.core.presentation.login + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword + +@Composable +fun UiPassword.colored( + numberColor: Color = MaterialTheme.colorScheme.primary, + symbolColor: Color = MaterialTheme.colorScheme.tertiary, +): AnnotatedString = remember(this, numberColor, symbolColor) { + buildAnnotatedString { + parts.forEach { + when (it) { + is UiPassword.Part.Letter -> append(it.text) + is UiPassword.Part.Number -> withStyle(SpanStyle(color = numberColor)) { + append(it.text) + } + + is UiPassword.Part.Symbol -> withStyle(SpanStyle(color = symbolColor)) { + append(it.text) + } + } + } + } +} diff --git a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt index 943f45eeb..e885cf630 100644 --- a/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt +++ b/feature/item/core/src/main/kotlin/de/davis/keygo/feature/item/core/presentation/login/model/UiPassword.kt @@ -1,13 +1,5 @@ package de.davis.keygo.feature.item.core.presentation.login.model -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle - data class UiPassword(val value: String) { val parts: List = value.splitByCharClassRegex() @@ -34,28 +26,8 @@ data class UiPassword(val value: String) { companion object { - val PATTERN = Regex("""\p{L}+|\d+|[^\p{L}\d]+""") - fun String.asUiPassword() = UiPassword(this) - } -} + private val PATTERN = Regex("""\p{L}+|\d+|[^\p{L}\d]+""") -@Composable -fun UiPassword.colored( - numberColor: Color = MaterialTheme.colorScheme.primary, - symbolColor: Color = MaterialTheme.colorScheme.tertiary -) = remember(this, numberColor, symbolColor) { - buildAnnotatedString { - parts.forEach { - when (it) { - is UiPassword.Part.Letter -> append(it.text) - is UiPassword.Part.Number -> withStyle(SpanStyle(color = numberColor)) { - append(it.text) - } - - is UiPassword.Part.Symbol -> withStyle(SpanStyle(color = symbolColor)) { - append(it.text) - } - } - } + fun String.asUiPassword() = UiPassword(this) } } diff --git a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt index 5f63da3f3..1b68c36a2 100644 --- a/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt +++ b/feature/item/create/src/main/kotlin/de/davis/keygo/feature/item/create/presentation/password/GeneratePasswordContent.kt @@ -36,7 +36,7 @@ import de.davis.keygo.core.ui.components.KeyGoCard import de.davis.keygo.core.ui.components.KeyGoCardProperties import de.davis.keygo.core.ui.theme.secretTextStyle import de.davis.keygo.core.util.presentation.ObserveAsEvents -import de.davis.keygo.feature.item.core.presentation.login.model.colored +import de.davis.keygo.feature.item.core.presentation.login.colored import de.davis.keygo.feature.item.create.R import de.davis.keygo.feature.item.create.presentation.password.model.GeneratePasswordUiEvent import de.davis.keygo.feature.item.create.presentation.password.model.UiCharacterSet diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index 605df477e..4741ddfb6 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -82,9 +82,9 @@ import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormField import de.davis.keygo.feature.item.core.presentation.component.KeyGoFormSuggestionField import de.davis.keygo.feature.item.core.presentation.copyableEntry import de.davis.keygo.feature.item.core.presentation.entry +import de.davis.keygo.feature.item.core.presentation.login.colored import de.davis.keygo.feature.item.core.presentation.login.model.FieldType import de.davis.keygo.feature.item.core.presentation.login.model.UiPassword.Companion.asUiPassword -import de.davis.keygo.feature.item.core.presentation.login.model.colored import de.davis.keygo.feature.item.core.presentation.transformation.TrimTransformation import de.davis.keygo.feature.item.core.presentation.transformation.rememberSchemeStrippingTransformation import de.davis.keygo.feature.item.view.R @@ -242,9 +242,10 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) } ) { val scrollState = rememberScrollState() + val uiPassword = remember(pwd.raw) { pwd.raw.asUiPassword() } Text( text = if (isPasswordHidden) AnnotatedString(pwd.hidden) - else pwd.raw.asUiPassword().colored(), + else uiPassword.colored(), style = secretTextStyle, maxLines = 1, modifier = Modifier.horizontalScroll(scrollState), From 6a0d716eefb46c23fb03dc2f40da99bf257e55ed Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 28 Aug 2026 13:21:28 +0200 Subject: [PATCH 11/11] refactor: move totp progress to trailing content --- .../item/view/login/ViewLoginContent.kt | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt index 4741ddfb6..3744dace6 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/ViewLoginContent.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets @@ -39,11 +40,11 @@ import androidx.compose.material.icons.filled.Sell import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.AlertDialog import androidx.compose.material3.AssistChip +import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -52,6 +53,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.WavyProgressIndicatorDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -96,6 +98,8 @@ import de.davis.keygo.feature.item.view.login.model.ViewLoginUiEvent import de.davis.keygo.feature.totp.domain.model.TotpValue import de.davis.keygo.feature.totp.presentation.component.QRScanner import de.davis.keygo.feature.totp.presentation.component.TotpParseErrorDialog +import kotlin.math.ceil +import kotlin.time.Duration.Companion.milliseconds import de.davis.keygo.core.item.R as CoreItemR import de.davis.keygo.core.ui.R as CoreUiR import de.davis.keygo.feature.item.core.R as ItemCoreR @@ -263,12 +267,27 @@ fun ViewLoginContent(state: ViewLoginState, onEvent: (ViewLoginUiEvent) -> Unit) leadingIcon = Icons.Default.AccessTime, dataToCopy = { state.totpState.value.code }, sensitive = true, + trailingContent = { + val indicatorSize = WavyProgressIndicatorDefaults.CircularContainerSize + Box( + modifier = Modifier.size(indicatorSize), + contentAlignment = Alignment.Center, + ) { + CircularWavyProgressIndicator( + progress = { progress.value }, + ) + + Text( + text = ceil(totpState.value.maxLifetime.milliseconds.inWholeSeconds * progress.value).toInt() + .toString(), + fontSize = with(LocalDensity.current) { (indicatorSize * 0.35f).toSp() }, + maxLines = 1, + color = MaterialTheme.colorScheme.primary, + ) + } + } ) { Text(text = state.totpState.formattedCode) - LinearProgressIndicator( - progress = { progress.value }, - modifier = Modifier.fillMaxWidth(), - ) } is TotpState.Error -> entry(