diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/add/AddScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/add/AddScreen.kt index 4d6dabb..416f3e5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/add/AddScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/add/AddScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.ArrowDropDown -import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.CloudDone import androidx.compose.material.icons.outlined.FileUpload import androidx.compose.material.icons.outlined.Folder @@ -41,7 +40,6 @@ import androidx.compose.material.icons.outlined.Password import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -64,9 +62,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.SpanStyle @@ -77,15 +73,11 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import com.artemchep.keyguard.common.model.Loadable import com.artemchep.keyguard.common.model.UsernameVariationIcon -import com.artemchep.keyguard.common.model.fold import com.artemchep.keyguard.common.model.titleH -import com.artemchep.keyguard.common.service.logging.LogRepository import com.artemchep.keyguard.feature.auth.common.TextFieldModel2 import com.artemchep.keyguard.feature.auth.common.VisibilityState import com.artemchep.keyguard.feature.auth.common.VisibilityToggle -import com.artemchep.keyguard.feature.home.vault.add.AddState import com.artemchep.keyguard.feature.home.vault.component.FlatItemTextContent2 import com.artemchep.keyguard.feature.home.vault.component.Section import com.artemchep.keyguard.feature.home.vault.component.VaultViewTotpBadge2 @@ -107,12 +99,12 @@ import com.artemchep.keyguard.ui.DropdownScopeImpl import com.artemchep.keyguard.ui.EmailFlatTextField import com.artemchep.keyguard.ui.ExpandedIfNotEmpty import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow +import com.artemchep.keyguard.ui.FakeFlatTextField import com.artemchep.keyguard.ui.FlatDropdown import com.artemchep.keyguard.ui.FlatItem import com.artemchep.keyguard.ui.FlatItemAction import com.artemchep.keyguard.ui.FlatItemLayout import com.artemchep.keyguard.ui.FlatItemTextContent -import com.artemchep.keyguard.ui.FlatSimpleNote import com.artemchep.keyguard.ui.FlatTextField import com.artemchep.keyguard.ui.FlatTextFieldBadge import com.artemchep.keyguard.ui.LeMOdelBottomSheet @@ -142,7 +134,6 @@ import com.artemchep.keyguard.ui.theme.isDark import com.artemchep.keyguard.ui.theme.monoFontFamily import com.artemchep.keyguard.ui.util.DividerColor import com.artemchep.keyguard.ui.util.HorizontalDivider -import com.artemchep.keyguard.ui.util.VerticalDivider import dev.icerock.moko.resources.compose.stringResource import kotlinx.collections.immutable.ImmutableList @@ -1086,104 +1077,44 @@ private fun DateMonthYearField( item: AddStateItem.DateMonthYear<*>, ) { val state by item.state.flow.collectAsState() - BiFlatContainer( + val isEmpty by derivedStateOf { + val isEmpty = state.month.state.value.isEmpty() && + state.year.state.value.isEmpty() + isEmpty + } + val onClear = remember { + // lambda + { + state.month.state.value = "" + state.year.state.value = "" + } + } + FakeFlatTextField( modifier = modifier .padding(horizontal = Dimens.horizontalPadding), - contentModifier = Modifier - .clickable( - indication = LocalIndication.current, - interactionSource = remember { - MutableInteractionSource() - }, - role = Role.Button, - ) { - state.onClick.invoke() - }, - isError = rememberUpdatedState(newValue = false), - isFocused = rememberUpdatedState(newValue = false), - isEmpty = rememberUpdatedState(newValue = false), - label = { - Text( - text = item.label, - style = MaterialTheme.typography.bodySmall, - ) - }, - content = { - val density = LocalDensity.current + label = item.label, + value = { Row( - modifier = Modifier - .graphicsLayer { - translationY = 12f + density.density - }, + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(2.dp), ) { - Row( - modifier = Modifier - .heightIn(min = BiFlatValueHeightMin) - .weight(1f, fill = false), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - val month = state.month.text.ifBlank { "--" } - val year = state.year.text.ifBlank { "----" } - Text( - text = month, - ) - Text( - text = "/", - color = LocalContentColor.current - .combineAlpha(DisabledEmphasisAlpha), - ) - Text( - text = year, - ) - } -// Spacer( -// modifier = Modifier -// .width(4.dp), -// ) -// Icon( -// modifier = Modifier -// .alpha(MediumEmphasisAlpha), -// imageVector = Icons.Outlined.ArrowDropDown, -// contentDescription = null, -// ) - } - }, - trailing = { - val isEmpty = state.month.state.value.isEmpty() && - state.year.state.value.isEmpty() - ExpandedIfNotEmptyForRow( - valueOrNull = Unit.takeUnless { isEmpty }, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer( - modifier = Modifier - .width(8.dp), - ) - VerticalDivider( - modifier = Modifier - .height(24.dp), - ) - Spacer( - modifier = Modifier - .width(8.dp), - ) - IconButton( - enabled = true, - onClick = { - state.month.state.value = "" - state.year.state.value = "" - }, - ) { - Icon( - imageVector = Icons.Outlined.Clear, - contentDescription = null, - ) - } - } + val month = state.month.text.ifBlank { "--" } + val year = state.year.text.ifBlank { "----" } + Text( + text = month, + ) + Text( + text = "/", + color = LocalContentColor.current + .combineAlpha(DisabledEmphasisAlpha), + ) + Text( + text = year, + ) } }, + onClick = state.onClick, + onClear = onClear.takeIf { !isEmpty }, ) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/TextItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/TextItem.kt index bd3875f..2e553f3 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/TextItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/TextItem.kt @@ -9,6 +9,9 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -72,6 +75,7 @@ import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType @@ -296,6 +300,189 @@ fun ConcealedFlatTextField( ) } +@Composable +fun FakeFlatTextField( + modifier: Modifier = Modifier, + label: String? = null, + error: String? = null, + value: @Composable () -> Unit, + onClick: (() -> Unit)?, + onClear: (() -> Unit)?, + textStyle: TextStyle = LocalTextStyle.current, + leading: (@Composable RowScope.() -> Unit)? = null, + trailing: (@Composable RowScope.() -> Unit)? = null, + content: (@Composable ColumnScope.() -> Unit)? = null, +) { + val isError = error != null + var hasFocus by remember { + mutableStateOf(false) + } + + val updatedOnClick by rememberUpdatedState(onClick) + FlatTextFieldSurface( + modifier = modifier + .onFocusChanged { state -> + hasFocus = state.hasFocus + }, + isError = isError, + isFocused = hasFocus, + ) { + Column( + modifier = Modifier + .clickable( + indication = LocalIndication.current, + enabled = onClick != null, + interactionSource = remember { + MutableInteractionSource() + }, + role = Role.Button, + ) { + updatedOnClick?.invoke() + }, + ) { + Row( + modifier = Modifier + .padding( + start = 16.dp, + end = 8.dp, + top = 8.dp, + bottom = 8.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + val disabledAlphaTarget = if (onClick != null) 1f else DisabledEmphasisAlpha + val disabledAlphaState = animateFloatAsState(disabledAlphaTarget) + + if (leading != null) { + Row( + modifier = Modifier + .graphicsLayer { + alpha = disabledAlphaState.value + }, + verticalAlignment = Alignment.CenterVertically, + ) { + leading() + } + Spacer( + modifier = Modifier + .width(16.dp), + ) + } + Column( + modifier = Modifier + .weight(1f), + ) { + val expanded = onClear != null + TextFieldLabelLayout( + modifier = Modifier + .heightIn(min = 50.dp), + expanded = onClear != null, + ) { + if (label != null) { + TextFieldLabel( + modifier = Modifier + .fillMaxWidth() + .animateContentSize() + .graphicsLayer { + alpha = disabledAlphaState.value + }, + text = label, + expanded = expanded, + hasFocus = hasFocus, + hasError = isError, + ) + } + + Box { + androidx.compose.animation.AnimatedVisibility( + modifier = Modifier + .fillMaxWidth(), + enter = fadeIn(), + exit = fadeOut(), + visible = expanded, + ) { + // If color is not provided via the text style, use content color as a default + val textColor = run { + val color = + contentColorFor(backgroundColor = MaterialTheme.colorScheme.background) + val alpha = + if (onClick != null) DefaultEmphasisAlpha else DisabledEmphasisAlpha + color.copy(alpha = alpha) + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + ProvideTextStyle(mergedTextStyle) { + value() + } + } + } + } + Column( + modifier = Modifier + .padding( + end = 8.dp, + ), + ) { + ExpandedIfNotEmpty( + valueOrNull = error, + ) { text -> + FlatTextFieldBadgeLegacy( + error = text, + badge = null, + ) + } + if (content != null) { + content() + } + } + } + if (trailing != null) { + Spacer( + modifier = Modifier + .width(8.dp), + ) + Row( + modifier = Modifier + .graphicsLayer { + alpha = disabledAlphaState.value + }, + verticalAlignment = Alignment.CenterVertically, + ) { + trailing() + } + } + ExpandedIfNotEmptyForRow( + valueOrNull = onClear, + ) { lambda -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer( + modifier = Modifier + .width(8.dp), + ) + VerticalDivider( + modifier = Modifier + .height(24.dp), + ) + Spacer( + modifier = Modifier + .width(8.dp), + ) + IconButton( + onClick = lambda, + ) { + Icon( + imageVector = Icons.Outlined.Clear, + contentDescription = null, + ) + } + } + } + } + } + } +} + @Composable fun FlatTextField( modifier: Modifier = Modifier, @@ -376,24 +563,7 @@ fun FlatTextField( expanded = focused, ) { if (label != null) { - val focusedTextSize = MaterialTheme.typography.bodySmall.fontSize.value - val normalTextSize = LocalTextStyle.current.fontSize.value - val textSize by animateFloatAsState( - targetValue = if (focused) focusedTextSize else normalTextSize, - ) - - val focusedTextColor = when { - isError -> MaterialTheme.colorScheme.error - hasFocus -> MaterialTheme.colorScheme.primary - else -> LocalContentColor.current - } - val normalTextColor = LocalContentColor.current - .combineAlpha(HighEmphasisAlpha) - val textColor by animateColorAsState( - targetValue = if (focused) focusedTextColor else normalTextColor, - ) - - Text( + TextFieldLabel( modifier = Modifier .fillMaxWidth() .animateContentSize() @@ -401,9 +571,9 @@ fun FlatTextField( alpha = disabledAlphaState.value }, text = label, - fontSize = textSize.sp, - maxLines = 1, - color = textColor, + expanded = focused, + hasFocus = hasFocus, + hasError = isError, ) } @@ -627,6 +797,40 @@ private data class Afh( val createdAt: Instant, ) +@Composable +private fun TextFieldLabel( + modifier: Modifier = Modifier, + text: String, + expanded: Boolean, + hasFocus: Boolean, + hasError: Boolean, +) { + val expandedTextSize = MaterialTheme.typography.bodySmall.fontSize.value + val normalTextSize = LocalTextStyle.current.fontSize.value + val textSize by animateFloatAsState( + targetValue = if (expanded) expandedTextSize else normalTextSize, + ) + + val expandedTextColor = when { + hasError -> MaterialTheme.colorScheme.error + hasFocus -> MaterialTheme.colorScheme.primary + else -> LocalContentColor.current + } + val normalTextColor = LocalContentColor.current + .combineAlpha(HighEmphasisAlpha) + val textColor by animateColorAsState( + targetValue = if (expanded) expandedTextColor else normalTextColor, + ) + + Text( + modifier = modifier, + text = text, + fontSize = textSize.sp, + maxLines = 1, + color = textColor, + ) +} + @Composable private fun TextFieldLabelLayout( modifier: Modifier = Modifier,