From 8e27ad9072e53e2bea96859701f81fe0537a3d97 Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Thu, 11 Apr 2024 14:39:30 +0300 Subject: [PATCH] improvement: Rework Add item screens to use lazy column #238 --- .../keyguard/feature/add/AddScreen.kt | 71 ++--- .../feature/home/vault/add/AddScreen.kt | 291 ++++++++---------- .../feature/send/add/SendAddScreen.kt | 110 +++++-- .../com/artemchep/keyguard/ui/TextItem.kt | 4 + 4 files changed, 232 insertions(+), 244 deletions(-) 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 0730f3b3..f2bca1ff 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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActionScope @@ -143,51 +144,43 @@ private val paddingValues = PaddingValues( ) context(AddScreenScope) -@Composable -fun ColumnScope.AddScreenItems( +fun LazyListScope.AddScreenItems( ) { - SkeletonTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Dimens.horizontalPadding), - ) - SkeletonTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Dimens.horizontalPadding), - ) - SkeletonText( - modifier = Modifier - .fillMaxWidth(0.75f) - .padding(horizontal = Dimens.horizontalPadding), - style = MaterialTheme.typography.labelMedium, - ) - SkeletonText( - modifier = Modifier - .fillMaxWidth(0.4f) - .padding(horizontal = Dimens.horizontalPadding), - style = MaterialTheme.typography.labelMedium, - ) -} - -context(AddScreenScope) -@Composable -fun ColumnScope.AddScreenItems( - items: List, -) { - items.forEach { - key(it.id) { - AnyField( - modifier = Modifier, - item = it, - ) - } + item("items.skeleton.1") { + SkeletonTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Dimens.horizontalPadding), + ) + } + item("items.skeleton.2") { + SkeletonTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Dimens.horizontalPadding), + ) + } + item("items.skeleton.3") { + SkeletonText( + modifier = Modifier + .fillMaxWidth(0.75f) + .padding(horizontal = Dimens.horizontalPadding), + style = MaterialTheme.typography.labelMedium, + ) + } + item("items.skeleton.4") { + SkeletonText( + modifier = Modifier + .fillMaxWidth(0.4f) + .padding(horizontal = Dimens.horizontalPadding), + style = MaterialTheme.typography.labelMedium, + ) } } context(AddScreenScope) @Composable -private fun AnyField( +fun AnyField( modifier: Modifier = Modifier, item: AddStateItem, ) = when (item) { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt index b8abbc5e..b73ca85a 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddScreen.kt @@ -2,162 +2,52 @@ package com.artemchep.keyguard.feature.home.vault.add -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.KeyboardActionScope -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items 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 -import androidx.compose.material.icons.outlined.Key -import androidx.compose.material.icons.outlined.Password import androidx.compose.material.icons.outlined.Save import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api 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 -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -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 arrow.core.partially1 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.getOrNull -import com.artemchep.keyguard.common.model.titleH -import com.artemchep.keyguard.common.service.logging.LogRepository import com.artemchep.keyguard.feature.add.AddScreenItems import com.artemchep.keyguard.feature.add.AddScreenScope +import com.artemchep.keyguard.feature.add.AnyField import com.artemchep.keyguard.feature.add.ToolbarContent import com.artemchep.keyguard.feature.add.ToolbarContentItemErrSkeleton -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.filepicker.FilePickerEffect -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 -import com.artemchep.keyguard.feature.navigation.LocalNavigationController import com.artemchep.keyguard.feature.navigation.NavigationIcon -import com.artemchep.keyguard.feature.navigation.NavigationIntent -import com.artemchep.keyguard.feature.qr.ScanQrButton import com.artemchep.keyguard.res.Res -import com.artemchep.keyguard.ui.AutofillButton -import com.artemchep.keyguard.ui.BiFlatContainer -import com.artemchep.keyguard.ui.BiFlatTextField -import com.artemchep.keyguard.ui.BiFlatTextFieldLabel -import com.artemchep.keyguard.ui.BiFlatValueHeightMin -import com.artemchep.keyguard.ui.DefaultEmphasisAlpha import com.artemchep.keyguard.ui.DefaultFab -import com.artemchep.keyguard.ui.DisabledEmphasisAlpha -import com.artemchep.keyguard.ui.DropdownMenuItemFlat -import com.artemchep.keyguard.ui.DropdownMinWidth -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.FabState -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 -import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.OptionsButton -import com.artemchep.keyguard.ui.PasswordFlatTextField -import com.artemchep.keyguard.ui.PasswordPwnedBadge -import com.artemchep.keyguard.ui.PasswordStrengthBadge -import com.artemchep.keyguard.ui.ScaffoldColumn -import com.artemchep.keyguard.ui.SimpleNote -import com.artemchep.keyguard.ui.UrlFlatTextField +import com.artemchep.keyguard.ui.ScaffoldLazyColumn import com.artemchep.keyguard.ui.button.FavouriteToggleButton -import com.artemchep.keyguard.ui.focus.focusRequester2 -import com.artemchep.keyguard.ui.icons.IconBox -import com.artemchep.keyguard.ui.icons.KeyguardAttachment -import com.artemchep.keyguard.ui.icons.KeyguardCollection -import com.artemchep.keyguard.ui.icons.KeyguardOrganization -import com.artemchep.keyguard.ui.icons.KeyguardTwoFa -import com.artemchep.keyguard.ui.icons.KeyguardWebsite -import com.artemchep.keyguard.ui.icons.icon -import com.artemchep.keyguard.ui.markdown.MarkdownText import com.artemchep.keyguard.ui.shimmer.shimmer import com.artemchep.keyguard.ui.skeleton.SkeletonText -import com.artemchep.keyguard.ui.skeleton.SkeletonTextField -import com.artemchep.keyguard.ui.text.annotatedResource -import com.artemchep.keyguard.ui.theme.Dimens -import com.artemchep.keyguard.ui.theme.combineAlpha -import com.artemchep.keyguard.ui.theme.isDark -import com.artemchep.keyguard.ui.theme.monoFontFamily import com.artemchep.keyguard.ui.toolbar.LargeToolbar -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.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import org.kodein.di.compose.rememberInstance @Composable fun AddScreen( @@ -186,14 +76,14 @@ fun AddScreen( ) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddScreenContent( addScreenScope: AddScreenScope, loadableState: Loadable, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - ScaffoldColumn( + ScaffoldLazyColumn( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, @@ -270,7 +160,7 @@ private fun AddScreenContent( }, ) }, - columnVerticalArrangement = Arrangement.spacedBy(8.dp), + listVerticalArrangement = Arrangement.spacedBy(8.dp), ) { populateItems( addScreenScope = addScreenScope, @@ -279,8 +169,7 @@ private fun AddScreenContent( } } -@Composable -private fun ColumnScope.populateItems( +private fun LazyListScope.populateItems( addScreenScope: AddScreenScope, loadableState: Loadable, ) = loadableState.fold( @@ -297,12 +186,89 @@ private fun ColumnScope.populateItems( }, ) -@Composable -private fun ColumnScope.populateItemsSkeleton( +private fun LazyListScope.populateItemsSkeleton( addScreenScope: AddScreenScope, +) { + item("ownership") { + AddScreenToolbarSkeletonItem() + } + item("ownership.section") { + Section() + } + item("items") { + Spacer( + modifier = Modifier + .height(24.dp), + ) + } + with(addScreenScope) { + AddScreenItems() + } +} + +private fun LazyListScope.populateItemsContent( + addScreenScope: AddScreenScope, + state: AddState, +) { + item("ownership") { + AddScreenToolbarItem( + state = state, + ) + } + item("ownership.section") { + Section() + } + if (state.merge != null) { + item("merge") { + AddScreenMergeItem( + modifier = Modifier, + state = state.merge, + ) + } + item("merge.section") { + Section() + } + } + item("items") { + Spacer( + modifier = Modifier + .height(24.dp), + ) + } + items( + items = state.items, + key = { it.id }, + ) { item -> + with(addScreenScope) { + AnyField( + modifier = Modifier, + item = item, + ) + } + } +} + +@Composable +private fun AddScreenToolbarItem( + modifier: Modifier = Modifier, + state: AddState, +) { + ToolbarContent( + modifier = modifier, + account = state.ownership.ui.account, + organization = state.ownership.ui.organization, + collection = state.ownership.ui.collection, + folder = state.ownership.ui.folder, + onClick = state.ownership.ui.onClick, + ) +} + +@Composable +private fun AddScreenToolbarSkeletonItem( + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding( horizontal = 8.dp, @@ -336,59 +302,42 @@ private fun ColumnScope.populateItemsSkeleton( fraction = 0.35f, ) } - Section() - Spacer(Modifier.height(24.dp)) - with(addScreenScope) { - AddScreenItems() - } } @Composable -private fun ColumnScope.populateItemsContent( - addScreenScope: AddScreenScope, - state: AddState, +private fun AddScreenMergeItem( + modifier: Modifier = Modifier, + state: AddState.Merge, +) = Column( + modifier = modifier, ) { - ToolbarContent( - modifier = Modifier, - account = state.ownership.ui.account, - organization = state.ownership.ui.organization, - collection = state.ownership.ui.collection, - folder = state.ownership.ui.folder, - onClick = state.ownership.ui.onClick, + ExpandedIfNotEmpty( + valueOrNull = state.note, + ) { note -> + FlatSimpleNote( + modifier = Modifier, + note = note, + ) + } + Spacer( + modifier = Modifier + .height(8.dp), ) - Section() - if (state.merge != null) { - ExpandedIfNotEmpty( - valueOrNull = state.merge.note, - ) { note -> - FlatSimpleNote( - modifier = Modifier, - note = note, + FlatItemLayout( + leading = { + Checkbox( + checked = state.removeOrigin.checked, + onCheckedChange = null, ) - } - FlatItemLayout( - leading = { - Checkbox( - checked = state.merge.removeOrigin.checked, - onCheckedChange = null, - ) - }, - content = { - Text( - text = stringResource(Res.strings.additem_merge_remove_origin_ciphers_title), - ) - }, - onClick = { - val newValue = !state.merge.removeOrigin.checked - state.merge.removeOrigin.onChange?.invoke(newValue) - }, - ) - Section() - } - Spacer(Modifier.height(24.dp)) - with(addScreenScope) { - AddScreenItems( - items = state.items, - ) - } + }, + content = { + Text( + text = stringResource(Res.strings.additem_merge_remove_origin_ciphers_title), + ) + }, + onClick = { + val newValue = !state.removeOrigin.checked + state.removeOrigin.onChange?.invoke(newValue) + }, + ) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/add/SendAddScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/add/SendAddScreen.kt index 36cacc44..1dc0080a 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/add/SendAddScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/add/SendAddScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Save import androidx.compose.material3.Checkbox @@ -27,6 +29,7 @@ import com.artemchep.keyguard.common.model.fold import com.artemchep.keyguard.common.model.getOrNull import com.artemchep.keyguard.feature.add.AddScreenItems import com.artemchep.keyguard.feature.add.AddScreenScope +import com.artemchep.keyguard.feature.add.AnyField import com.artemchep.keyguard.feature.add.ToolbarContent import com.artemchep.keyguard.feature.add.ToolbarContentItemErrSkeleton import com.artemchep.keyguard.feature.home.vault.add.AddState @@ -40,6 +43,7 @@ import com.artemchep.keyguard.ui.FlatItemLayout import com.artemchep.keyguard.ui.FlatSimpleNote import com.artemchep.keyguard.ui.OptionsButton import com.artemchep.keyguard.ui.ScaffoldColumn +import com.artemchep.keyguard.ui.ScaffoldLazyColumn import com.artemchep.keyguard.ui.button.FavouriteToggleButton import com.artemchep.keyguard.ui.shimmer.shimmer import com.artemchep.keyguard.ui.skeleton.SkeletonText @@ -72,7 +76,7 @@ fun SendAddScreen( loadableState: Loadable, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - ScaffoldColumn( + ScaffoldLazyColumn( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, @@ -128,7 +132,7 @@ fun SendAddScreen( }, ) }, - columnVerticalArrangement = Arrangement.spacedBy(8.dp), + listVerticalArrangement = Arrangement.spacedBy(8.dp), ) { populateItems( addScreenScope = addScreenScope, @@ -137,8 +141,7 @@ fun SendAddScreen( } } -@Composable -private fun ColumnScope.populateItems( +private fun LazyListScope.populateItems( addScreenScope: AddScreenScope, loadableState: Loadable, ) = loadableState.fold( @@ -155,12 +158,78 @@ private fun ColumnScope.populateItems( }, ) -@Composable -private fun ColumnScope.populateItemsSkeleton( +private fun LazyListScope.populateItemsSkeleton( addScreenScope: AddScreenScope, +) { + item("ownership") { + AddScreenToolbarSkeletonItem() + } + item("ownership.section") { + Section() + } + item("items") { + Spacer( + modifier = Modifier + .height(24.dp), + ) + } + with(addScreenScope) { + AddScreenItems() + } +} + +private fun LazyListScope.populateItemsContent( + addScreenScope: AddScreenScope, + state: SendAddState, +) { + item("ownership") { + AddScreenToolbarItem( + state = state, + ) + } + item("ownership.section") { + Section() + } + item("items") { + Spacer( + modifier = Modifier + .height(24.dp), + ) + } + items( + items = state.items, + key = { it.id }, + ) { item -> + with(addScreenScope) { + AnyField( + modifier = Modifier, + item = item, + ) + } + } +} + +@Composable +private fun AddScreenToolbarItem( + modifier: Modifier = Modifier, + state: SendAddState, +) { + ToolbarContent( + modifier = modifier, + account = state.ownership.ui.account, + organization = state.ownership.ui.organization, + collection = state.ownership.ui.collection, + folder = state.ownership.ui.folder, + onClick = state.ownership.ui.onClick, + ) +} + +@Composable +private fun AddScreenToolbarSkeletonItem( + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding( horizontal = 8.dp, @@ -175,31 +244,4 @@ private fun ColumnScope.populateItemsSkeleton( fraction = 0.5f, ) } - Section() - Spacer(Modifier.height(24.dp)) - with(addScreenScope) { - AddScreenItems() - } -} - -@Composable -private fun ColumnScope.populateItemsContent( - addScreenScope: AddScreenScope, - state: SendAddState, -) { - ToolbarContent( - modifier = Modifier, - account = state.ownership.ui.account, - organization = state.ownership.ui.organization, - collection = state.ownership.ui.collection, - folder = state.ownership.ui.folder, - onClick = state.ownership.ui.onClick, - ) - Section() - Spacer(Modifier.height(24.dp)) - with(addScreenScope) { - AddScreenItems( - items = state.items, - ) - } } 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 2e553f39..67c60a66 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/TextItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/TextItem.kt @@ -691,6 +691,10 @@ fun FlatTextField( val optionsText = rememberUpdatedState(value.text) val optionsList = rememberUpdatedState(value.autocompleteOptions) + if (optionsList.value.isEmpty()) { + return@Column + } + val options by remember { val valueFlow = snapshotFlow { optionsText.value } .debounce(80L)