From 93bac97fb810bfa96950e7d0c779c0e33447ab2e Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Sun, 11 Feb 2024 22:08:07 +0200 Subject: [PATCH] feat: Material3 v1.2 with tone-based surfaces --- .../keyguard/android/BaseActivity.kt | 18 +- .../feature/attachments/AttachmentsScreen.kt | 49 ++- .../attachments/compose/ItemAttachment.kt | 16 +- .../feature/generator/GeneratorScreen.kt | 137 ++++--- .../keyguard/feature/home/HomeScreen.kt | 149 ++++--- .../vault/collections/CollectionsScreen.kt | 3 +- .../home/vault/component/VaultListItem.kt | 14 + .../home/vault/folders/FoldersScreen.kt | 3 +- .../organizations/OrganizationsScreen.kt | 2 +- .../home/vault/screen/VaultListScreen.kt | 113 +++++- .../vault/screen/VaultListStateProducer.kt | 6 +- .../feature/navigation/NavigationNode.kt | 5 +- .../feature/onboarding/OnboardingScreen.kt | 190 +++++---- .../feature/search/filter/FilterScreen.kt | 24 +- .../search/filter/component/FilterItem.kt | 3 +- .../feature/send/list/SendListScreen.kt | 126 ++++-- .../keyguard/feature/twopane/TwoPaneLayout.kt | 177 +++++---- .../keyguard/feature/twopane/TwoPaneScreen.kt | 129 +++++-- .../feature/watchtower/WatchtowerScreen.kt | 365 +++++++++++++++--- .../keyguard/ui/PasswordFilterItem.kt | 218 ++++++----- .../com/artemchep/keyguard/ui/Scaffold.kt | 18 +- .../keyguard/ui/surface/SurfaceColor.kt | 22 ++ .../keyguard/ui/surface/SurfaceElevation.kt | 59 +++ .../com/artemchep/keyguard/ui/theme/Theme.kt | 5 + .../keyguard/ui/toolbar/CustomToolbar.kt | 9 +- .../keyguard/ui/toolbar/LargeToolbar.kt | 10 + .../keyguard/ui/toolbar/SmallToolbar.kt | 15 + .../keyguard/ui/toolbar/util/ToolbarColors.kt | 31 ++ .../kotlin/com/artemchep/keyguard/Main.kt | 4 +- gradle/libs.versions.toml | 4 +- 30 files changed, 1333 insertions(+), 591 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceColor.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceElevation.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/util/ToolbarColors.kt diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/BaseActivity.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/BaseActivity.kt index 23acdd0b..e11d1a15 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/android/BaseActivity.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/android/BaseActivity.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -45,6 +46,7 @@ import com.artemchep.keyguard.feature.navigation.N import com.artemchep.keyguard.feature.navigation.NavigationController import com.artemchep.keyguard.feature.navigation.NavigationIntent import com.artemchep.keyguard.feature.navigation.NavigationRouterBackHandler +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.KeyguardTheme import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase @@ -98,7 +100,7 @@ abstract class BaseActivity : AppCompatActivity(), DIAware { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { KeyguardTheme { - val containerColor = MaterialTheme.colorScheme.background + val containerColor = MaterialTheme.colorScheme.surfaceContainerHighest val contentColor = contentColorFor(containerColor) Surface( modifier = Modifier.semantics { @@ -110,8 +112,12 @@ abstract class BaseActivity : AppCompatActivity(), DIAware { color = containerColor, contentColor = contentColor, ) { - Navigation { - Content() + CompositionLocalProvider( + LocalSurfaceColor provides containerColor, + ) { + Navigation { + Content() + } } } } @@ -157,7 +163,11 @@ abstract class BaseActivity : AppCompatActivity(), DIAware { ) = kotlin.run { when (intent) { is NavigationIntent.NavigateToPreview -> handleNavigationIntent(intent, showMessage) - is NavigationIntent.NavigateToPreviewInFileManager -> handleNavigationIntent(intent, showMessage) + is NavigationIntent.NavigateToPreviewInFileManager -> handleNavigationIntent( + intent, + showMessage, + ) + is NavigationIntent.NavigateToSend -> handleNavigationIntent(intent, showMessage) is NavigationIntent.NavigateToLargeType -> handleNavigationIntent(intent, showMessage) is NavigationIntent.NavigateToShare -> handleNavigationIntent(intent, showMessage) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/AttachmentsScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/AttachmentsScreen.kt index 9d452bfe..fae92fb1 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/AttachmentsScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/AttachmentsScreen.kt @@ -6,7 +6,9 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import com.artemchep.keyguard.common.model.Loadable @@ -23,13 +25,33 @@ import com.artemchep.keyguard.ui.DefaultSelection import com.artemchep.keyguard.ui.ScaffoldLazyColumn import com.artemchep.keyguard.ui.skeleton.SkeletonItem import com.artemchep.keyguard.ui.toolbar.LargeToolbar +import com.artemchep.keyguard.ui.toolbar.SmallToolbar import dev.icerock.moko.resources.compose.stringResource @Composable fun AttachmentsScreen() { val loadableState = produceAttachmentsScreenState() - + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() TwoPaneScreen( + header = { modifier -> + SmallToolbar( + modifier = modifier, + title = { + Text( + text = stringResource(Res.strings.downloads), + ) + }, + navigationIcon = { + NavigationIcon() + }, + ) + + SideEffect { + if (scrollBehavior.state.heightOffsetLimit != 0f) { + scrollBehavior.state.heightOffsetLimit = 0f + } + } + }, detail = { modifier -> VaultHomeScreenFilterPaneCard2( modifier = modifier, @@ -37,11 +59,12 @@ fun AttachmentsScreen() { onClear = loadableState.getOrNull()?.filter?.onClear, ) }, - ) { modifier, detailIsVisible -> + ) { modifier, tabletUi -> AttachmentsScreen( modifier = modifier, state = loadableState, - showFilter = !detailIsVisible, + tabletUi = tabletUi, + scrollBehavior = scrollBehavior, ) } } @@ -55,14 +78,18 @@ fun AttachmentsScreen() { fun AttachmentsScreen( modifier: Modifier, state: Loadable, - showFilter: Boolean, + tabletUi: Boolean, + scrollBehavior: TopAppBarScrollBehavior, ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() ScaffoldLazyColumn( modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, topBar = { + if (tabletUi) { + return@ScaffoldLazyColumn + } + LargeToolbar( title = { Text( @@ -73,13 +100,11 @@ fun AttachmentsScreen( NavigationIcon() }, actions = { - if (showFilter) { - VaultHomeScreenFilterButton2( - modifier = modifier, - items = state.getOrNull()?.filter?.items.orEmpty(), - onClear = state.getOrNull()?.filter?.onClear, - ) - } + VaultHomeScreenFilterButton2( + modifier = modifier, + items = state.getOrNull()?.filter?.items.orEmpty(), + onClear = state.getOrNull()?.filter?.onClear, + ) }, scrollBehavior = scrollBehavior, ) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt index 1699fe7b..02a6abf1 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt @@ -45,6 +45,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.artemchep.keyguard.feature.attachments.SelectableItemState import com.artemchep.keyguard.feature.attachments.model.AttachmentItem @@ -247,7 +249,7 @@ private fun ItemAttachmentLayout( if (selectable.selected) { MaterialTheme.colorScheme.primaryContainer } else { - MaterialTheme.colorScheme.surface + Color.Unspecified } FlatDropdown( modifier = modifier, @@ -255,12 +257,20 @@ private fun ItemAttachmentLayout( content = { FlatItemTextContent( title = { - Text(name) + Text( + text = name, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + ) }, text = if (size != null) { // composable { - Text(size) + Text( + text = size, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) } } else { null diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt index afb85a82..c10914e1 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt @@ -46,7 +46,9 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -60,6 +62,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -69,6 +72,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -128,47 +132,23 @@ fun GeneratorScreen( args = args, ) + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val sliderInteractionSource = remember { MutableInteractionSource() } TwoPaneScreen( - detail = { modifier -> - GeneratorPaneDetail( - modifier = modifier, - loadableState = loadableState, - sliderInteractionSource = sliderInteractionSource, - ) - }, - ) { modifier, detailIsVisible -> - GeneratorPaneMaster( - modifier = modifier, - loadableState = loadableState, - showFilter = !detailIsVisible, - sliderInteractionSource = sliderInteractionSource, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun GeneratorPaneDetail( - modifier: Modifier = Modifier, - loadableState: Loadable, - sliderInteractionSource: MutableInteractionSource, -) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - ScaffoldColumn( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - topAppBarScrollBehavior = scrollBehavior, - topBar = { + header = { modifier -> SmallToolbar( + modifier = modifier, + containerColor = Color.Transparent, title = { Text( - text = stringResource(Res.strings.filter_header_title), - style = MaterialTheme.typography.titleMedium, + text = stringResource(Res.strings.generator_header_title), ) }, + navigationIcon = { + NavigationIcon() + }, actions = { loadableState.fold( ifLoading = { @@ -190,9 +170,44 @@ private fun GeneratorPaneDetail( }, ) }, - scrollBehavior = scrollBehavior, + ) + + SideEffect { + if (scrollBehavior.state.heightOffsetLimit != 0f) { + scrollBehavior.state.heightOffsetLimit = 0f + } + } + }, + detail = { modifier -> + GeneratorPaneDetail( + modifier = modifier, + loadableState = loadableState, + sliderInteractionSource = sliderInteractionSource, ) }, + ) { modifier, tabletUi -> + GeneratorPaneMaster( + modifier = modifier, + loadableState = loadableState, + tabletUi = tabletUi, + scrollBehavior = scrollBehavior, + sliderInteractionSource = sliderInteractionSource, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun GeneratorPaneDetail( + modifier: Modifier = Modifier, + loadableState: Loadable, + sliderInteractionSource: MutableInteractionSource, +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + ScaffoldColumn( + modifier = modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + topAppBarScrollBehavior = scrollBehavior, ) { loadableState.fold( ifLoading = { @@ -234,15 +249,19 @@ private fun ColumnScope.GeneratorPaneDetailContent( private fun GeneratorPaneMaster( modifier: Modifier, loadableState: Loadable, - showFilter: Boolean, + tabletUi: Boolean, + scrollBehavior: TopAppBarScrollBehavior, sliderInteractionSource: MutableInteractionSource, ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() ScaffoldColumn( modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, topBar = { + if (tabletUi) { + return@ScaffoldColumn + } + LargeToolbar( title = { Text(stringResource(Res.strings.generator_header_title)) @@ -251,27 +270,25 @@ private fun GeneratorPaneMaster( NavigationIcon() }, actions = { - if (showFilter) { - loadableState.fold( - ifLoading = { - }, - ifOk = { state -> - val updatedOnOpenHistory by rememberUpdatedState(state.onOpenHistory) - IconButton( - onClick = { - updatedOnOpenHistory() - }, - ) { - Icon( - imageVector = Icons.Outlined.History, - contentDescription = null, - ) - } - val actions = state.options - OptionsButton(actions) - }, - ) - } + loadableState.fold( + ifLoading = { + }, + ifOk = { state -> + val updatedOnOpenHistory by rememberUpdatedState(state.onOpenHistory) + IconButton( + onClick = { + updatedOnOpenHistory() + }, + ) { + Icon( + imageVector = Icons.Outlined.History, + contentDescription = null, + ) + } + val actions = state.options + OptionsButton(actions) + }, + ) }, scrollBehavior = scrollBehavior, ) @@ -326,7 +343,7 @@ private fun GeneratorPaneMaster( ifOk = { state -> GeneratorPaneMasterContent( state = state, - showFilter = showFilter, + tabletUi = tabletUi, sliderInteractionSource = sliderInteractionSource, ) }, @@ -337,10 +354,10 @@ private fun GeneratorPaneMaster( @Composable private fun ColumnScope.GeneratorPaneMasterContent( state: GeneratorState, - showFilter: Boolean, + tabletUi: Boolean, sliderInteractionSource: MutableInteractionSource, ) { - if (showFilter) { + if (!tabletUi) { GeneratorType( state = state, ) @@ -355,7 +372,7 @@ private fun ColumnScope.GeneratorPaneMasterContent( filterFlow = state.filterState, ) - if (showFilter) { + if (!tabletUi) { GeneratorFilterItems( filterFlow = state.filterState, sliderInteractionSource = sliderInteractionSource, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt index 8244d3dd..98e865c2 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt @@ -2,7 +2,6 @@ package com.artemchep.keyguard.feature.home import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -55,19 +53,20 @@ import androidx.compose.material.icons.outlined.Send import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemColors +import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.Surface +import androidx.compose.material3.NavigationRailItemColors +import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf @@ -82,14 +81,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import com.artemchep.keyguard.common.io.attempt -import com.artemchep.keyguard.common.io.bind import com.artemchep.keyguard.common.model.DAccountStatus import com.artemchep.keyguard.common.usecase.GetAccountStatus import com.artemchep.keyguard.common.usecase.GetNavLabel -import com.artemchep.keyguard.common.usecase.GetPasswordStrength -import com.artemchep.keyguard.core.store.DatabaseManager -import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher import com.artemchep.keyguard.feature.generator.GeneratorRoute import com.artemchep.keyguard.feature.watchtower.WatchtowerRoute import com.artemchep.keyguard.feature.home.settings.SettingsRoute @@ -113,20 +107,20 @@ import com.artemchep.keyguard.platform.leIme import com.artemchep.keyguard.platform.leNavigationBars import com.artemchep.keyguard.platform.leStatusBars import com.artemchep.keyguard.platform.leSystemBars -import com.artemchep.keyguard.platform.recordLog import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.ui.ExpandedIfNotEmpty import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.icons.ChevronIcon import com.artemchep.keyguard.ui.shimmer.shimmer +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.Dimens import com.artemchep.keyguard.ui.theme.badgeContainer import com.artemchep.keyguard.ui.theme.combineAlpha import com.artemchep.keyguard.ui.theme.info import com.artemchep.keyguard.ui.theme.infoContainer import com.artemchep.keyguard.ui.theme.ok -import com.artemchep.keyguard.ui.util.VerticalDivider import dev.icerock.moko.resources.compose.stringResource +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -149,7 +143,7 @@ fun HomeScreen( navBarVisible: Boolean = true, ) { val navRoutes = remember { - listOf( + persistentListOf( Rail( route = vaultRoute, icon = Icons.Outlined.Home, @@ -179,12 +173,6 @@ fun HomeScreen( iconSelected = Icons.Filled.Security, label = TextHolder.Res(Res.strings.home_watchtower_label), ), -// Rail( -// route = AccountsRoute, -// icon = Icons.Outlined.AccountCircle, -// iconSelected = Icons.Filled.AccountCircle, -// label = "Accounts", -// ), Rail( route = SettingsRoute, icon = Icons.Outlined.Settings, @@ -208,18 +196,17 @@ fun HomeScreen( @Composable fun HomeScreenContent( backStack: PersistentList, - routes: List, + routes: ImmutableList, navBarVisible: Boolean = true, ) { ResponsiveLayout { val horizontalInsets = WindowInsets.leStatusBars .union(WindowInsets.leNavigationBars) .union(WindowInsets.leDisplayCutout) - .only(WindowInsetsSides.Horizontal) + .only(WindowInsetsSides.Start) Row( modifier = Modifier - .windowInsetsPadding(horizontalInsets) - .consumeWindowInsets(horizontalInsets), + .windowInsetsPadding(horizontalInsets), ) { val getNavLabel by rememberInstance() val navLabelState = remember(getNavLabel) { @@ -248,6 +235,7 @@ fun HomeScreenContent( // When the keyboard is opened, there might be not // enough space for all the items. .verticalScroll(scrollState), + containerColor = Color.Transparent, windowInsets = verticalInsets, ) { routes.forEach { r -> @@ -284,7 +272,6 @@ fun HomeScreenContent( ) } } - VerticalDivider() } } val bottomInsets = WindowInsets.leStatusBars @@ -307,18 +294,18 @@ fun HomeScreenContent( }, ), ) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f), + val defaultContainerColor = MaterialTheme.colorScheme.surfaceContainer + CompositionLocalProvider( + LocalSurfaceColor provides defaultContainerColor, + LocalNavigationNodeVisualStack provides persistentListOf(), ) { - CompositionLocalProvider( - LocalNavigationNodeVisualStack provides persistentListOf(), - ) { - NavigationNode( - entries = backStack, - ) - } + NavigationNode( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background(defaultContainerColor), + entries = backStack, + ) } // TODO: @@ -360,46 +347,44 @@ fun HomeScreenContent( AnimatedVisibility( visible = bottomNavBarVisible, ) { - Surface( - tonalElevation = 3.dp, + Column( + modifier = Modifier, ) { - Column { - BannerStatusBadge( - modifier = Modifier - .fillMaxWidth(), - statusState = accountStatusState, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottomInsets.asPaddingValues()) - .height(80.dp) - .selectableGroup(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - routes.forEach { r -> - BottomNavigationControllerItem( - backStack = backStack, - route = r.route, - icon = r.icon, - iconSelected = r.iconSelected, - label = if (navLabelState.value) { - // composable - { - Text( - text = textResource(r.label), - maxLines = 1, - textAlign = TextAlign.Center, - // Default style does not fit on devices with small - // screens. - style = MaterialTheme.typography.labelSmall, - ) - } - } else { - null - }, - ) - } + BannerStatusBadge( + modifier = Modifier + .fillMaxWidth(), + statusState = accountStatusState, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottomInsets.asPaddingValues()) + .height(80.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + routes.forEach { r -> + BottomNavigationControllerItem( + backStack = backStack, + route = r.route, + icon = r.icon, + iconSelected = r.iconSelected, + label = if (navLabelState.value) { + // composable + { + Text( + text = textResource(r.label), + maxLines = 1, + textAlign = TextAlign.Center, + // Default style does not fit on devices with small + // screens. + style = MaterialTheme.typography.labelSmall, + ) + } + } else { + null + }, + ) } } @@ -731,7 +716,7 @@ private fun RailStatusBadgeContent( @Composable private fun ColumnScope.RailNavigationControllerItem( - backStack: List, + backStack: ImmutableList, route: Route, icon: ImageVector, iconSelected: ImageVector, @@ -753,6 +738,7 @@ private fun ColumnScope.RailNavigationControllerItem( }, label = label, selected = selected, + colors = navigationRailItemColors(), onClick = { navigateOnClick(controller, backStack, route) }, @@ -761,7 +747,7 @@ private fun ColumnScope.RailNavigationControllerItem( @Composable private fun RowScope.BottomNavigationControllerItem( - backStack: List, + backStack: ImmutableList, route: Route, icon: ImageVector, iconSelected: ImageVector, @@ -783,12 +769,23 @@ private fun RowScope.BottomNavigationControllerItem( }, label = label, selected = selected, + colors = navigationBarItemColors(), onClick = { navigateOnClick(controller, backStack, route) }, ) } +@Composable +private fun navigationRailItemColors(): NavigationRailItemColors { + return NavigationRailItemDefaults.colors() +} + +@Composable +private fun navigationBarItemColors(): NavigationBarItemColors { + return NavigationBarItemDefaults.colors() +} + private fun isSelected( backStack: List, route: Route, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/collections/CollectionsScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/collections/CollectionsScreen.kt index a1d82b52..6a2470b5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/collections/CollectionsScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/collections/CollectionsScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -192,7 +193,7 @@ private fun OrganizationsScreenCollectionItem( item: CollectionsState.Content.Item.Collection, ) { val backgroundColor = - if (item.selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface + if (item.selected) MaterialTheme.colorScheme.primaryContainer else Color.Unspecified FlatDropdown( modifier = modifier, backgroundColor = backgroundColor, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt index 650e9368..93416abc 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultListItem.kt @@ -84,6 +84,7 @@ import com.artemchep.keyguard.ui.icons.IconSmallBox import com.artemchep.keyguard.ui.icons.KeyguardAttachment import com.artemchep.keyguard.ui.icons.KeyguardFavourite import com.artemchep.keyguard.ui.rightClickable +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.Dimens import com.artemchep.keyguard.ui.theme.combineAlpha import com.artemchep.keyguard.ui.theme.isDark @@ -633,6 +634,19 @@ fun surfaceColorAtElevation(color: Color, elevation: Dp): Color { } } +@Composable +fun ColorScheme.localSurfaceColorAtElevation( + surface: Color, + elevation: Dp, +): Color { + val tint = surfaceColorAtElevationSemi(elevation = elevation) + return if (tint.isSpecified) { + tint.compositeOver(surface) + } else { + surface + } +} + /** * Returns the [ColorScheme.surface] color with an alpha of the [ColorScheme.surfaceTint] color * overlaid on top of it. diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/folders/FoldersScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/folders/FoldersScreen.kt index c4736d75..42d22288 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/folders/FoldersScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/folders/FoldersScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -194,7 +195,7 @@ private fun FoldersScreenFolderItem( item: FoldersState.Content.Item.Folder, ) { val backgroundColor = - if (item.selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface + if (item.selected) MaterialTheme.colorScheme.primaryContainer else Color.Unspecified FlatDropdown( modifier = modifier, backgroundColor = backgroundColor, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/organizations/OrganizationsScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/organizations/OrganizationsScreen.kt index 19432fec..eb050203 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/organizations/OrganizationsScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/organizations/OrganizationsScreen.kt @@ -167,7 +167,7 @@ private fun OrganizationsScreenItem( item: OrganizationsState.Content.Item, ) { val backgroundColor = - if (item.selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface + if (item.selected) MaterialTheme.colorScheme.primaryContainer else Color.Unspecified FlatDropdown( modifier = modifier, backgroundColor = backgroundColor, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListScreen.kt index 1b5f6530..f341bb42 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListScreen.kt @@ -2,6 +2,7 @@ package com.artemchep.keyguard.feature.home.vault.screen import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add @@ -35,12 +37,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -161,13 +165,85 @@ fun VaultListScreen( val focusRequester = remember { FocusRequester2() } TwoPaneScreen( + header = { modifier -> + val title = args.appBar?.title + val subtitle = args.appBar?.subtitle + val hasTitle = title != null || subtitle != null + if (hasTitle) { + Row( + modifier = Modifier + .heightIn(min = 64.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(4.dp)) + NavigationIcon() + Spacer(Modifier.width(4.dp)) + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically), + ) { + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = LocalContentColor.current + .combineAlpha(MediumEmphasisAlpha), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } + if (title != null) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } + Spacer(Modifier.width(4.dp)) + VaultListSortButton( + state = state, + ) + OptionsButton( + actions = state.actions, + ) + Spacer(Modifier.width(4.dp)) + } + } else { + } + SearchTextField( + modifier = Modifier + .focusRequester2(focusRequester), + text = state.query.state.value, + placeholder = stringResource(Res.strings.vault_main_search_placeholder), + searchIcon = !hasTitle, + leading = { + // Do nothing + }, + trailing = { + if (!hasTitle) { + VaultListSortButton( + state = state, + ) + OptionsButton( + actions = state.actions, + ) + } + }, + onTextChange = state.query.onChange, + onGoClick = null, + ) + //Spacer(Modifier.height(16.dp)) + }, detail = { modifier -> VaultListFilterScreen( modifier = modifier, state = state, ) }, - ) { modifier, detailIsVisible -> + ) { modifier, tabletUi -> VaultHomeScreenListPane( modifier = modifier, state = state, @@ -175,7 +251,7 @@ fun VaultListScreen( title = args.appBar?.title, subtitle = args.appBar?.subtitle, fab = args.canAddSecrets, - showFilter = !detailIsVisible, + tabletUi = tabletUi, preselect = args.preselect, ) } @@ -257,7 +333,7 @@ fun VaultHomeScreenListPane( title: String?, subtitle: String?, fab: Boolean, - showFilter: Boolean, + tabletUi: Boolean, preselect: Boolean, ) { val itemsState = (state.content as? VaultListState.Content.Items) @@ -338,6 +414,10 @@ fun VaultHomeScreenListPane( .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, topBar = { + if (tabletUi) { + return@ScaffoldLazyColumn + } + CustomToolbar( scrollBehavior = scrollBehavior, ) { @@ -376,22 +456,21 @@ fun VaultHomeScreenListPane( ) } } - if (showFilter) { - Spacer(Modifier.width(4.dp)) - VaultListFilterButton( - state = state, - ) - VaultListSortButton( - state = state, - ) - OptionsButton( - actions = state.actions, - ) - } + Spacer(Modifier.width(4.dp)) + VaultListFilterButton( + state = state, + ) + VaultListSortButton( + state = state, + ) + OptionsButton( + actions = state.actions, + ) Spacer(Modifier.width(4.dp)) } } else { } + SearchTextField( modifier = Modifier .focusRequester2(focusRequester), @@ -407,7 +486,7 @@ fun VaultHomeScreenListPane( Row( verticalAlignment = Alignment.CenterVertically, ) { - if (!hasTitle && showFilter) { + if (!hasTitle) { VaultListFilterButton( state = state, ) @@ -594,7 +673,7 @@ fun VaultHomeScreenListPane( items = list, key = { model -> model.id }, ) { model -> - if (model is VaultItem2.QuickFilters && showFilter) { + if (model is VaultItem2.QuickFilters && !tabletUi) { Box( modifier = Modifier .animateItemPlacement(), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt index abbf19f3..90b4cffe 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultListStateProducer.kt @@ -718,7 +718,7 @@ fun vaultListScreenState( } } section { - FlatItemAction( + this += FlatItemAction( icon = Icons.Outlined.Info, title = translate(Res.strings.ciphers_view_details), trailing = { @@ -755,7 +755,7 @@ fun vaultListScreenState( ) } section { - FlatItemAction( + this += FlatItemAction( icon = Icons.Outlined.Info, title = translate(Res.strings.ciphers_view_details), trailing = { @@ -779,7 +779,7 @@ fun vaultListScreenState( ) } section { - FlatItemAction( + this += FlatItemAction( icon = Icons.Outlined.Info, title = translate(Res.strings.ciphers_view_details), trailing = { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationNode.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationNode.kt index 6e6ce620..458312a0 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationNode.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/NavigationNode.kt @@ -62,6 +62,7 @@ private data class Foo( fun NavigationNode( entries: PersistentList, offset: Int = 0, + modifier: Modifier = Modifier, ) { val getNavAnimation by rememberInstance() @@ -79,7 +80,9 @@ fun NavigationNode( val logicalStack = LocalNavigationNodeLogicalStack.current val visualStack = LocalNavigationNodeVisualStack.current - Box { + Box( + modifier = modifier, + ) { // // Draw screen stack // diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt index bb612738..65fdd8f5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/onboarding/OnboardingScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.LaunchedEffect 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.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.Hyphens @@ -261,74 +262,69 @@ fun OnboardingCard( premium: Boolean = false, imageVector: ImageVector? = null, ) { - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.medium, - tonalElevation = 0.dp, + Box( + modifier = modifier + .clip(MaterialTheme.shapes.medium), + contentAlignment = Alignment.TopEnd, ) { - Box( - modifier = Modifier, - contentAlignment = Alignment.TopEnd, - ) { - if (imageVector != null) { - Icon( - imageVector, - modifier = Modifier - .padding(8.dp) - .size(72.dp) - .alpha(0.035f) - .align(Alignment.TopEnd), - contentDescription = null, - ) - } - Column( + if (imageVector != null) { + Icon( + imageVector, modifier = Modifier .padding(8.dp) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { + .size(72.dp) + .alpha(0.035f) + .align(Alignment.TopEnd), + contentDescription = null, + ) + } + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + .copy( + hyphens = Hyphens.Auto, + lineBreak = LineBreak.Heading, + ), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + ExpandedIfNotEmpty(valueOrNull = text) { Text( - text = title, - style = MaterialTheme.typography.titleMedium + text = it, + style = MaterialTheme.typography.bodyMedium .copy( hyphens = Hyphens.Auto, - lineBreak = LineBreak.Heading, + lineBreak = LineBreak.Paragraph, ), - maxLines = 3, + maxLines = 6, overflow = TextOverflow.Ellipsis, ) - ExpandedIfNotEmpty(valueOrNull = text) { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium - .copy( - hyphens = Hyphens.Auto, - lineBreak = LineBreak.Paragraph, - ), - maxLines = 6, - overflow = TextOverflow.Ellipsis, - ) - } - ExpandedIfNotEmpty( - valueOrNull = Unit.takeIf { premium }, + } + ExpandedIfNotEmpty( + valueOrNull = Unit.takeIf { premium }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - modifier = Modifier - .size(12.dp), - imageVector = Icons.Outlined.KeyguardPremium, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringResource(Res.strings.feat_keyguard_premium_label), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelSmall, - ) - } + Icon( + modifier = Modifier + .size(12.dp), + imageVector = Icons.Outlined.KeyguardPremium, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(Res.strings.feat_keyguard_premium_label), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelSmall, + ) } } } @@ -342,57 +338,51 @@ fun SmallOnboardingCard( text: String? = null, imageVector: ImageVector? = null, ) { - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.medium, - tonalElevation = 0.dp, - border = BorderStroke(Dp.Hairline, DividerColor), + Box( + modifier = modifier + .clip(MaterialTheme.shapes.medium), + contentAlignment = Alignment.TopEnd, ) { - Box( - modifier = Modifier, - contentAlignment = Alignment.TopEnd, - ) { - if (imageVector != null) { - Icon( - imageVector, - modifier = Modifier - .padding(8.dp) - .size(72.dp) - .alpha(0.035f) - .align(Alignment.TopEnd), - contentDescription = null, - ) - } - Column( + if (imageVector != null) { + Icon( + imageVector, modifier = Modifier .padding(8.dp) - .widthIn(max = 128.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { + .size(72.dp) + .alpha(0.035f) + .align(Alignment.TopEnd), + contentDescription = null, + ) + } + Column( + modifier = Modifier + .padding(8.dp) + .widthIn(max = 128.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall + .copy( + hyphens = Hyphens.Auto, + lineBreak = LineBreak.Heading, + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + ExpandedIfNotEmpty(valueOrNull = text) { Text( - text = title, - style = MaterialTheme.typography.titleSmall + text = it, + style = MaterialTheme.typography.bodySmall .copy( hyphens = Hyphens.Auto, - lineBreak = LineBreak.Heading, + lineBreak = LineBreak.Paragraph, ), - maxLines = 2, + color = LocalContentColor.current + .combineAlpha(MediumEmphasisAlpha), + maxLines = 4, overflow = TextOverflow.Ellipsis, ) - ExpandedIfNotEmpty(valueOrNull = text) { - Text( - text = it, - style = MaterialTheme.typography.bodySmall - .copy( - hyphens = Hyphens.Auto, - lineBreak = LineBreak.Paragraph, - ), - color = LocalContentColor.current - .combineAlpha(MediumEmphasisAlpha), - maxLines = 4, - overflow = TextOverflow.Ellipsis, - ) - } } } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/FilterScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/FilterScreen.kt index f4f4b3eb..84bf3d5f 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/FilterScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/FilterScreen.kt @@ -33,18 +33,18 @@ fun FilterScreen( modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, - topBar = { - SmallToolbar( - title = { - Text( - text = stringResource(Res.strings.filter_header_title), - style = MaterialTheme.typography.titleMedium, - ) - }, - actions = actions, - scrollBehavior = scrollBehavior, - ) - }, +// topBar = { +// SmallToolbar( +// title = { +// Text( +// text = stringResource(Res.strings.filter_header_title), +// style = MaterialTheme.typography.titleMedium, +// ) +// }, +// actions = actions, +// scrollBehavior = scrollBehavior, +// ) +// }, floatingActionState = run { val fabState = if (onClear != null) { FabState( diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/component/FilterItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/component/FilterItem.kt index d1aa82be..c2e22469 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/component/FilterItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/search/filter/component/FilterItem.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics @@ -83,7 +84,7 @@ fun FilterItemLayout( val backgroundColor = if (checked) { MaterialTheme.colorScheme.selectedContainer - } else MaterialTheme.colorScheme.surface + } else Color.Transparent Surface( modifier = modifier .semantics { role = Role.Checkbox }, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt index ae272a3e..96604924 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/list/SendListScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AccessTime import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.PersonAdd +import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api @@ -26,6 +27,7 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect @@ -154,30 +156,6 @@ fun SendListScreen() { controller.queue(intent) } - TwoPaneScreen( - detail = { modifier -> - SendListFilterScreen( - modifier = modifier, - state = state, - ) - }, - ) { modifier, detailIsVisible -> - SendScreenContent( - modifier = modifier, - state = state, - showFilter = !detailIsVisible, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SendScreenContent( - modifier: Modifier = Modifier, - state: SendListState, - showFilter: Boolean, -) { - val focusRequester = remember { FocusRequester2() } @@ -188,12 +166,90 @@ private fun SendScreenContent( }, ) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + TwoPaneScreen( + header = { + Row( + modifier = Modifier + .heightIn(min = 64.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(4.dp)) + NavigationIcon() + Spacer(Modifier.width(4.dp)) + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically), + ) { + Text( + text = stringResource(Res.strings.send_main_header_title), + style = MaterialTheme.typography.titleMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + Spacer(Modifier.width(4.dp)) + SendListSortButton( + state = state, + ) + OptionsButton( + actions = state.actions, + ) + Spacer(Modifier.width(4.dp)) + } + + SearchTextField( + modifier = Modifier + .focusRequester2(focusRequester), + text = state.query.state.value, + placeholder = stringResource(Res.strings.send_main_search_placeholder), + searchIcon = false, + leading = { + }, + trailing = { + }, + onTextChange = state.query.onChange, + onGoClick = null, + ) + }, + detail = { modifier -> + SendListFilterScreen( + modifier = modifier, + state = state, + ) + }, + ) { modifier, tabletUi -> + SendScreenContent( + modifier = modifier, + state = state, + tabletUi = tabletUi, + focusRequester = focusRequester, + pullRefreshState = pullRefreshState, + scrollBehavior = scrollBehavior, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SendScreenContent( + modifier: Modifier = Modifier, + state: SendListState, + tabletUi: Boolean, + focusRequester: FocusRequester2, + pullRefreshState: PullRefreshState, + scrollBehavior: TopAppBarScrollBehavior, +) { ScaffoldLazyColumn( modifier = modifier .pullRefresh(pullRefreshState) .nestedScroll(scrollBehavior.nestedScrollConnection), topAppBarScrollBehavior = scrollBehavior, topBar = { + if (tabletUi) { + return@ScaffoldLazyColumn + } + CustomToolbar( scrollBehavior = scrollBehavior, ) { @@ -218,18 +274,16 @@ private fun SendScreenContent( maxLines = 1, ) } - if (showFilter) { - Spacer(Modifier.width(4.dp)) - SendListFilterButton( - state = state, - ) - SendListSortButton( - state = state, - ) - OptionsButton( - actions = state.actions, - ) - } + Spacer(Modifier.width(4.dp)) + SendListFilterButton( + state = state, + ) + SendListSortButton( + state = state, + ) + OptionsButton( + actions = state.actions, + ) Spacer(Modifier.width(4.dp)) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneLayout.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneLayout.kt index af9871bb..ef680eff 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneLayout.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneLayout.kt @@ -7,9 +7,19 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Lock import androidx.compose.material3.Icon @@ -27,6 +37,7 @@ import androidx.compose.runtime.rememberUpdatedState 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.draw.clipToBounds import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -42,7 +53,17 @@ import com.artemchep.keyguard.feature.navigation.NavigationAnimation import com.artemchep.keyguard.feature.navigation.NavigationAnimationType import com.artemchep.keyguard.feature.navigation.transform import com.artemchep.keyguard.platform.LocalAnimationFactor +import com.artemchep.keyguard.platform.leDisplayCutout +import com.artemchep.keyguard.platform.leNavigationBars +import com.artemchep.keyguard.platform.leStatusBars +import com.artemchep.keyguard.ui.scaffoldContentWindowInsets import com.artemchep.keyguard.ui.screenMaxWidth +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor +import com.artemchep.keyguard.ui.surface.LocalSurfaceElevation +import com.artemchep.keyguard.ui.surface.color +import com.artemchep.keyguard.ui.surface.splitHigh +import com.artemchep.keyguard.ui.surface.splitLow +import com.artemchep.keyguard.ui.surface.surfaceElevationColor import com.artemchep.keyguard.ui.util.VerticalDivider import org.kodein.di.compose.rememberInstance @@ -109,91 +130,113 @@ fun TwoPaneScaffoldScope.TwoPaneLayout( detailPane: (@Composable BoxScope.() -> Unit)? = null, masterPane: @Composable BoxScope.() -> Unit, ) { + val surfaceElevation = LocalSurfaceElevation.current val getNavAnimation by rememberInstance() - - Row { + Row( + modifier = Modifier + .background(surfaceElevationColor(surfaceElevation.from)), + ) { if (this@TwoPaneLayout.tabletUi) { - val absoluteElevation = LocalAbsoluteTonalElevation.current + 1.dp + val elevation = surfaceElevation.splitLow() CompositionLocalProvider( - LocalAbsoluteTonalElevation provides absoluteElevation, - LocalHasDetailPane provides (detailPane != null), + LocalSurfaceElevation provides elevation, + LocalSurfaceColor provides surfaceElevationColor(elevation.to), + LocalHasDetailPane provides true, ) { + val horizontalInsets = scaffoldContentWindowInsets + .only(WindowInsetsSides.Horizontal) PaneLayout( modifier = Modifier + .consumeWindowInsets(horizontalInsets) .widthIn(max = this@TwoPaneLayout.masterPaneWidth), ) { masterPane(this) } } - VerticalDivider() } key("movable-pane") { - PaneLayout( - modifier = Modifier - .background(MaterialTheme.colorScheme.background), + val elevation = if (this@TwoPaneLayout.tabletUi) { + surfaceElevation.splitHigh() + } else { + surfaceElevation + } + CompositionLocalProvider( + LocalSurfaceElevation provides elevation, + LocalSurfaceColor provides surfaceElevationColor(elevation.to), + LocalHasDetailPane provides true, ) { - val pane = detailPane ?: masterPane.takeUnless { this@TwoPaneLayout.tabletUi } - val updatedAnimationScale by rememberUpdatedState(LocalAnimationFactor) - AnimatedContent( + PaneLayout( modifier = Modifier - .fillMaxSize(), - targetState = pane, - transitionSpec = { - val animationType = getNavAnimation().value - val transitionType = kotlin.run { - if ( - initialState == null || - targetState == null - ) { - return@run NavigationAnimationType.SWITCH - } - - val isForward = targetState === detailPane - if (isForward) { - NavigationAnimationType.GO_FORWARD - } else { - NavigationAnimationType.GO_BACKWARD - } - } - - NavigationAnimation.transform( - scale = updatedAnimationScale, - animationType = animationType, - transitionType = transitionType, - ) - }, - label = "", - ) { foo -> - Box( + .background(surfaceElevation.color), + ) { + val pane = detailPane ?: masterPane.takeUnless { this@TwoPaneLayout.tabletUi } + val updatedAnimationScale by rememberUpdatedState(LocalAnimationFactor) + AnimatedContent( modifier = Modifier .fillMaxSize(), - ) { - if (foo != null) { - foo() - } else { - Row( - modifier = Modifier - .align(Alignment.Center) - .alpha(0.035f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( + targetState = pane, + transitionSpec = { + val animationType = getNavAnimation().value + val transitionType = kotlin.run { + if ( + initialState == null || + targetState == null + ) { + return@run NavigationAnimationType.SWITCH + } + + val isForward = targetState === detailPane + if (isForward) { + NavigationAnimationType.GO_FORWARD + } else { + NavigationAnimationType.GO_BACKWARD + } + } + + NavigationAnimation.transform( + scale = updatedAnimationScale, + animationType = animationType, + transitionType = transitionType, + ) + }, + label = "", + ) { foo -> + Box( + modifier = Modifier + .fillMaxSize(), + ) { + if (foo != null) { + Box( modifier = Modifier - .size(48.dp), - imageVector = Icons.Outlined.Lock, - contentDescription = null, - tint = MaterialTheme.colorScheme.onBackground, - ) - Text( - text = remember { - keyguardSpan() - }, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.displayLarge, - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - ) + .fillMaxSize(), + ) { + foo() + } + } else { + Row( + modifier = Modifier + .align(Alignment.Center) + .alpha(0.035f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + modifier = Modifier + .size(48.dp), + imageVector = Icons.Outlined.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = remember { + keyguardSpan() + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.displayLarge, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + ) + } } } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneScreen.kt index 685fd060..743dec38 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/twopane/TwoPaneScreen.kt @@ -1,18 +1,41 @@ package com.artemchep.keyguard.feature.twopane +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width -import androidx.compose.material3.LocalAbsoluteTonalElevation +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -import com.artemchep.keyguard.ui.util.VerticalDivider +import com.artemchep.keyguard.platform.leDisplayCutout +import com.artemchep.keyguard.platform.leNavigationBars +import com.artemchep.keyguard.platform.leStatusBars +import com.artemchep.keyguard.platform.leSystemBars +import com.artemchep.keyguard.ui.surface.LocalSurfaceElevation +import com.artemchep.keyguard.ui.surface.ProvideSurfaceColor +import com.artemchep.keyguard.ui.surface.splitLow +import com.artemchep.keyguard.ui.surface.surfaceElevationColor +import com.artemchep.keyguard.ui.theme.Dimens +import com.artemchep.keyguard.ui.theme.horizontalPaddingHalf @Composable fun TwoPaneScreen( modifier: Modifier = Modifier, + header: @Composable ColumnScope.(Modifier) -> Unit, detail: @Composable TwoPaneScaffoldScope.(Modifier) -> Unit, content: @Composable TwoPaneScaffoldScope.(Modifier, Boolean) -> Unit, ) { @@ -23,31 +46,83 @@ fun TwoPaneScreen( detailPaneMaxWidth = 320.dp, ratio = 0.4f, ) { - val scope = this - Row( - modifier = Modifier - .fillMaxSize(), - ) { - val detailIsVisible = this@TwoPaneScaffold.tabletUi - if (detailIsVisible) { - val absoluteElevation = LocalAbsoluteTonalElevation.current + 1.dp - CompositionLocalProvider( - LocalAbsoluteTonalElevation provides absoluteElevation, - ) { - detail( - scope, - Modifier - .width(scope.masterPaneWidth), - ) - } - VerticalDivider() - } + val surfaceElevation = LocalSurfaceElevation.current - content( - scope, - Modifier, - detailIsVisible, - ) + val scope = this + // In the tablet mode we "spill" the lower background color + // between the detail and content panels. + val color = kotlin.run { + val elevation = if (tabletUi) { + surfaceElevation.splitLow() + .to + } else { + surfaceElevation.to + } + surfaceElevationColor(elevation) + } + ProvideSurfaceColor(color) { + val detailIsVisible = this@TwoPaneScaffold.tabletUi + val insetsModifier = if (detailIsVisible) { + val insetsTop = WindowInsets.leSystemBars + .only(WindowInsetsSides.Top) + val insetsEnd = WindowInsets.leStatusBars + .union(WindowInsets.leNavigationBars) + .union(WindowInsets.leDisplayCutout) + .only(WindowInsetsSides.End) + Modifier + .windowInsetsPadding(insetsTop) + .windowInsetsPadding(insetsEnd) + } else { + Modifier + } + Column( + modifier = Modifier + .fillMaxSize() + .background(color) + .then(insetsModifier), + ) { + if (detailIsVisible) { + header.invoke(this, Modifier) + } + Row( + modifier = Modifier + .fillMaxSize(), + ) { + if (detailIsVisible) { + detail( + scope, + Modifier + .width(scope.masterPaneWidth), + ) + } + val contentModifier = if (detailIsVisible) { + val shapeModifier = kotlin.run { + val shape = MaterialTheme.shapes.large + .copy( + bottomStart = ZeroCornerSize, + bottomEnd = ZeroCornerSize, + ) + Modifier + .clip(shape) + } + val paddingModifier = Modifier + .padding(end = Dimens.horizontalPaddingHalf) + Modifier + .then(paddingModifier) + .then(shapeModifier) + } else { + Modifier + } + val detailSurfaceColor = surfaceElevationColor(surfaceElevation.to) + ProvideSurfaceColor(detailSurfaceColor) { + content( + scope, + contentModifier, + detailIsVisible, + ) + } + } + } } } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt index 8fa9f86e..f1b16529 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt @@ -8,10 +8,12 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.MutableWindowInsets import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -24,6 +26,7 @@ import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding @@ -43,6 +46,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.FolderOff import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Recycling import androidx.compose.material.icons.outlined.ShortText import androidx.compose.material.icons.outlined.Timer @@ -57,22 +61,34 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState 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.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.artemchep.keyguard.common.model.Loadable @@ -81,6 +97,7 @@ import com.artemchep.keyguard.common.model.fold import com.artemchep.keyguard.common.model.formatLocalized import com.artemchep.keyguard.feature.appreview.RequestAppReviewEffect import com.artemchep.keyguard.feature.home.vault.component.Section +import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevationSemi import com.artemchep.keyguard.feature.home.vault.model.FilterItem import com.artemchep.keyguard.feature.navigation.NavigationIcon import com.artemchep.keyguard.feature.search.filter.FilterButton @@ -95,10 +112,12 @@ import com.artemchep.keyguard.ui.DisabledEmphasisAlpha import com.artemchep.keyguard.ui.ExpandedIfNotEmpty import com.artemchep.keyguard.ui.FlatItem import com.artemchep.keyguard.ui.GridLayout +import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.OptionsButton import com.artemchep.keyguard.ui.animatedNumberText import com.artemchep.keyguard.ui.grid.preferredGridWidth import com.artemchep.keyguard.ui.icons.ChevronIcon +import com.artemchep.keyguard.ui.icons.IconSmallBox import com.artemchep.keyguard.ui.icons.KeyguardTwoFa import com.artemchep.keyguard.ui.icons.KeyguardWebsite import com.artemchep.keyguard.ui.poweredby.PoweredBy2factorauth @@ -108,11 +127,16 @@ import com.artemchep.keyguard.ui.scaffoldContentWindowInsets import com.artemchep.keyguard.ui.shimmer.shimmer import com.artemchep.keyguard.ui.skeleton.SkeletonItemPilled import com.artemchep.keyguard.ui.skeleton.SkeletonSection +import com.artemchep.keyguard.ui.skeleton.SkeletonText +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor +import com.artemchep.keyguard.ui.theme.Dimens import com.artemchep.keyguard.ui.theme.combineAlpha +import com.artemchep.keyguard.ui.theme.horizontalPaddingHalf import com.artemchep.keyguard.ui.theme.info import com.artemchep.keyguard.ui.theme.ok import com.artemchep.keyguard.ui.theme.warning import com.artemchep.keyguard.ui.toolbar.LargeToolbar +import com.artemchep.keyguard.ui.toolbar.SmallToolbar import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -132,24 +156,50 @@ fun WatchtowerScreen() { fun WatchtowerScreen( state: WatchtowerState, ) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() TwoPaneScreen( + header = { modifier -> + SmallToolbar( + modifier = modifier, + containerColor = Color.Transparent, + title = { + Text( + text = stringResource(Res.strings.watchtower_header_title), + ) + }, + navigationIcon = { + NavigationIcon() + }, + actions = { + OptionsButton( + actions = state.actions, + ) + }, + ) + + SideEffect { + if (scrollBehavior.state.heightOffsetLimit != 0f) { + scrollBehavior.state.heightOffsetLimit = 0f + } + } + }, detail = { modifier -> VaultHomeScreenFilterPaneCard( modifier = modifier, state = state, ) }, - ) { modifier, detailIsVisible -> + ) { modifier, tabletUi -> WatchtowerScreen2( modifier = modifier, state = state, - showFilter = !detailIsVisible, + tabletUi = tabletUi, + scrollBehavior = scrollBehavior, ) } } @OptIn( - ExperimentalMaterial3Api::class, androidx.compose.foundation.layout.ExperimentalLayoutApi::class, ) @Composable @@ -165,7 +215,6 @@ private fun VaultHomeScreenFilterPaneCard( } @OptIn( - ExperimentalMaterial3Api::class, androidx.compose.foundation.layout.ExperimentalLayoutApi::class, ) @Composable @@ -189,18 +238,22 @@ fun VaultHomeScreenFilterPaneCard2( fun WatchtowerScreen2( modifier: Modifier, state: WatchtowerState, - showFilter: Boolean, + tabletUi: Boolean, + scrollBehavior: TopAppBarScrollBehavior, ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val contentWindowInsets = scaffoldContentWindowInsets val remainingInsets = remember { MutableWindowInsets() } Scaffold( modifier = modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) .onConsumedWindowInsetsChanged { consumedWindowInsets -> remainingInsets.insets = contentWindowInsets.exclude(consumedWindowInsets) - } - .nestedScroll(scrollBehavior.nestedScrollConnection), + }, topBar = { + if (tabletUi) { + return@Scaffold + } + LargeToolbar( title = { Text( @@ -211,11 +264,9 @@ fun WatchtowerScreen2( NavigationIcon() }, actions = { - if (showFilter) { - VaultHomeScreenFilterButton( - state = state, - ) - } + VaultHomeScreenFilterButton( + state = state, + ) OptionsButton( actions = state.actions, ) @@ -223,6 +274,7 @@ fun WatchtowerScreen2( scrollBehavior = scrollBehavior, ) }, + containerColor = LocalSurfaceColor.current, contentWindowInsets = remainingInsets, ) { contentPadding -> ContentLayout( @@ -407,44 +459,240 @@ private fun ColumnScope.DashboardContent( private fun ColumnScope.DashboardContentData( content: WatchtowerState.Content.PasswordStrength, ) { - Section( - text = stringResource(Res.strings.watchtower_section_password_strength_label), + val total = content.items + .sumOf { it.count } + if (total == 0) { + return + } + + Spacer( + modifier = Modifier + .height(8.dp), ) - content.items.forEach { (t, u, onClick) -> - key(t) { - val score = when (t) { - PasswordStrength.Score.Weak -> 0f - PasswordStrength.Score.Fair -> 0.2f - PasswordStrength.Score.Good -> 0.5f - PasswordStrength.Score.Strong -> 0.9f - PasswordStrength.Score.VeryStrong -> 1f - } - FlatItem( - title = { - val text = t.formatLocalized() - Text(text) - }, - leading = { - val numberStr = animatedNumberText(u) - Ah( - score = score, - text = numberStr, - ) - }, - trailing = { - ChevronIcon() - }, - onClick = onClick, + Column( + modifier = Modifier + .padding( + horizontal = Dimens.horizontalPaddingHalf, ) + .clip(MaterialTheme.shapes.medium), + ) { + Spacer( + modifier = Modifier + .height(8.dp), + ) + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.titleMedium, + ) { + Text( + modifier = Modifier + .padding(horizontal = Dimens.horizontalPaddingHalf), + text = stringResource(Res.strings.watchtower_section_password_strength_label), + ) + } + Spacer( + modifier = Modifier + .height(8.dp), + ) + + val secondaryContainer = MaterialTheme.colorScheme.secondaryContainer + val errorContainer = MaterialTheme.colorScheme.errorContainer + Box( + modifier = Modifier + .height(24.dp) + .padding(horizontal = Dimens.horizontalPaddingHalf) + .fillMaxWidth() + .clip(MaterialTheme.shapes.small) + .drawBehind { + if (total == 0) { + return@drawBehind + } + + var x = 0f + + content.items.forEach { item -> + val score = when (item.score) { + PasswordStrength.Score.Weak -> 0f + PasswordStrength.Score.Fair -> 0.2f + PasswordStrength.Score.Good -> 0.5f + PasswordStrength.Score.Strong -> 0.9f + PasswordStrength.Score.VeryStrong -> 1f + } + val color = secondaryContainer + .copy(alpha = score) + .compositeOver(errorContainer) + val width = size.width * item.count / total.toFloat() + drawRect( + color = color, + topLeft = Offset( + x = x, + y = 0f, + ), + size = Size( + width = width.plus(1f) + .coerceAtMost(size.width), + height = size.height, + ), + ) + x += width + } + }, + ) { + } + Spacer( + modifier = Modifier + .height(8.dp), + ) + } + Spacer( + modifier = Modifier + .height(8.dp), + ) + FlowRow( + modifier = Modifier + .padding(horizontal = Dimens.horizontalPaddingHalf), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + content.items.forEach { item -> + if (item.count == 0) { + return@forEach + } + + val updatedOnClick by rememberUpdatedState(item.onClick) + val tintColor = MaterialTheme.colorScheme.surfaceColorAtElevationSemi(1.dp) + Row( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .background(tintColor) + .clickable(enabled = item.onClick != null) { + updatedOnClick?.invoke() + } + .padding( + start = 8.dp, + end = 8.dp, + top = 8.dp, + bottom = 8.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + val score = when (item.score) { + PasswordStrength.Score.Weak -> 0f + PasswordStrength.Score.Fair -> 0.2f + PasswordStrength.Score.Good -> 0.5f + PasswordStrength.Score.Strong -> 0.9f + PasswordStrength.Score.VeryStrong -> 1f + } + val numberStr = animatedNumberText(item.count) + Ah( + score = score, + text = numberStr, + ) + Spacer( + modifier = Modifier + .width(8.dp), + ) + val text = item.score.formatLocalized() + Text( + modifier = Modifier + .widthIn(max = 128.dp), + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium, + ) + } } } } @Composable private fun ColumnScope.DashboardContentSkeleton() { - SkeletonSection() - for (i in 0..4) - SkeletonItemPilled() + Spacer( + modifier = Modifier + .height(8.dp), + ) + Column( + modifier = Modifier + .padding( + horizontal = Dimens.horizontalPaddingHalf, + ) + .clip(MaterialTheme.shapes.medium), + ) { + Spacer( + modifier = Modifier + .height(8.dp), + ) + SkeletonText( + modifier = Modifier + .padding(horizontal = Dimens.horizontalPaddingHalf) + .fillMaxWidth(0.3f), + style = MaterialTheme.typography.titleMedium, + ) + Spacer( + modifier = Modifier + .height(8.dp), + ) + + val secondaryContainer = MaterialTheme.colorScheme.secondaryContainer + Box( + modifier = Modifier + .height(24.dp) + .padding(horizontal = Dimens.horizontalPaddingHalf) + .fillMaxWidth() + .shimmer() + .clip(MaterialTheme.shapes.small) + .background(secondaryContainer), + ) + Spacer( + modifier = Modifier + .height(8.dp), + ) + } + Spacer( + modifier = Modifier + .height(8.dp), + ) + FlowRow( + modifier = Modifier + .padding(horizontal = Dimens.horizontalPaddingHalf), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + for (i in 0..4) { + val tintColor = MaterialTheme.colorScheme + .surfaceColorAtElevationSemi(1.dp) + Row( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .background(tintColor) + .padding( + start = 8.dp, + end = 8.dp, + top = 8.dp, + bottom = 8.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .shimmer() + .height(18.dp) + .width(44.dp) + .clip(MaterialTheme.shapes.small) + .background(LocalContentColor.current.copy(alpha = 0.2f)), + ) + Spacer( + modifier = Modifier + .width(8.dp), + ) + SkeletonText( + modifier = Modifier + .width(56.dp), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } } // Cards @@ -843,7 +1091,7 @@ private fun ContentLayout( Column( modifier = Modifier - .widthIn(max = dashboardWidth) + // .widthIn(max = dashboardWidth) .fillMaxWidth(), ) { dashboardContent() @@ -907,10 +1155,18 @@ fun Card( content: (@Composable () -> Unit)? = null, onClick: (() -> Unit)? = null, ) { - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.medium, - tonalElevation = if (number > 0) 1.dp else 0.dp, + val backgroundModifier = if (number > 0) { + val tintColor = MaterialTheme.colorScheme.surfaceColorAtElevationSemi(1.dp) + Modifier + .background(tintColor) + } else { + Modifier + } + Box( + modifier = modifier + .clip(MaterialTheme.shapes.medium) + .then(backgroundModifier), + propagateMinConstraints = true, ) { if (imageVector != null) { Box( @@ -980,12 +1236,17 @@ fun Card( fun CardSkeleton( modifier: Modifier = Modifier, ) { + val backgroundModifier = run { + val tintColor = MaterialTheme.colorScheme.surfaceColorAtElevationSemi(1.dp) + Modifier + .background(tintColor) + } val contentColor = LocalContentColor.current.copy(alpha = DisabledEmphasisAlpha) - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.medium, - tonalElevation = 1.dp, + Box( + modifier = modifier + .clip(MaterialTheme.shapes.medium) + .then(backgroundModifier), ) { Column( modifier = Modifier diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt index baa3025b..92397fc5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/PasswordFilterItem.kt @@ -50,7 +50,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.isUnspecified import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback @@ -61,8 +63,10 @@ import androidx.compose.ui.unit.sp import arrow.core.andThen import com.artemchep.keyguard.common.usecase.CopyText import com.artemchep.keyguard.feature.home.vault.component.VaultItemIcon2 +import com.artemchep.keyguard.feature.home.vault.component.localSurfaceColorAtElevation import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevationSemi import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.combineAlpha import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList @@ -424,11 +428,22 @@ fun FlatItemLayout( .fillMaxWidth() .padding(paddingValues), ) { - val finalBackgroundColor = backgroundColor - .takeIf { it.isSpecified } - // default surface color - ?: MaterialTheme.colorScheme.surface - val animatedElevation by animateDpAsState(targetValue = elevation) + val backgroundModifier = kotlin.run { + // Check if there's actually a background color + // to render. + if ( + backgroundColor.isUnspecified && + elevation == 0.dp + ) { + return@run Modifier + } + + val bg = backgroundColor.takeIf { it.isSpecified } + ?: Color.Transparent + val fg = MaterialTheme.colorScheme.surfaceColorAtElevationSemi(elevation) + Modifier + .background(fg.compositeOver(bg)) + } val shape = MaterialTheme.shapes.medium val shapeBottomCornerDp by kotlin.run { @@ -439,119 +454,112 @@ fun FlatItemLayout( } animateDpAsState(targetValue = target) } - Surface( - shape = shape.copy( - bottomStart = CornerSize(shapeBottomCornerDp), - bottomEnd = CornerSize(shapeBottomCornerDp), - ), - color = finalBackgroundColor, - tonalElevation = animatedElevation, - ) { - val haptic by rememberUpdatedState(LocalHapticFeedback.current) - val updatedOnClick by rememberUpdatedState(onClick) - val updatedOnLongClick by rememberUpdatedState(onLongClick) - Column( - modifier = Modifier - .then( - if ((onClick != null || onLongClick != null) && enabled) { - Modifier - .combinedClickable( - onLongClick = if (onLongClick != null) { - // lambda - { - updatedOnLongClick?.invoke() - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } - } else { - null - }, - ) { - updatedOnClick?.invoke() - } - .rightClickable { - updatedOnLongClick?.invoke() - } - } else { - Modifier - }, + + val haptic by rememberUpdatedState(LocalHapticFeedback.current) + val updatedOnClick by rememberUpdatedState(onClick) + val updatedOnLongClick by rememberUpdatedState(onLongClick) + + Row( + modifier = Modifier + .clip( + shape.copy( + bottomStart = CornerSize(shapeBottomCornerDp), + bottomEnd = CornerSize(shapeBottomCornerDp), ), - ) { - Row( - modifier = Modifier - .minimumInteractiveComponentSize() - .padding(contentPadding), - verticalAlignment = Alignment.CenterVertically, - ) { - CompositionLocalProvider( - LocalContentColor provides LocalContentColor.current - .let { color -> - if (enabled) { - color + ) + .then(backgroundModifier) + .then( + if ((onClick != null || onLongClick != null) && enabled) { + Modifier + .combinedClickable( + onLongClick = if (onLongClick != null) { + // lambda + { + updatedOnLongClick?.invoke() + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } } else { - color.combineAlpha(alpha = DisabledEmphasisAlpha) - } - }, - ) { - if (leading != null) { - CompositionLocalProvider( - LocalMinimumInteractiveComponentEnforcement provides false, + null + }, ) { - leading() + updatedOnClick?.invoke() } - Spacer(Modifier.width(16.dp)) - } - Column( - modifier = Modifier - .weight(1f), - ) { - content() - } - if (trailing != null) { - Spacer(Modifier.width(16.dp)) - trailing() + .rightClickable { + updatedOnLongClick?.invoke() + } + } else { + Modifier + }, + ) + .minimumInteractiveComponentSize() + .padding(contentPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider( + LocalContentColor provides LocalContentColor.current + .let { color -> + if (enabled) { + color + } else { + color.combineAlpha(alpha = DisabledEmphasisAlpha) } + }, + ) { + if (leading != null) { + CompositionLocalProvider( + LocalMinimumInteractiveComponentEnforcement provides false, + ) { + leading() } + Spacer(Modifier.width(16.dp)) + } + Column( + modifier = Modifier + .weight(1f), + ) { + content() + } + if (trailing != null) { + Spacer(Modifier.width(16.dp)) + trailing() } } } actions.forEachIndexed { actionIndex, action -> + val actionShape = if (actions.size - 1 == actionIndex) { + shape.copy( + topStart = flatItemSmallCornerSize, + topEnd = flatItemSmallCornerSize, + ) + } else { + flatItemSmallShape + } + Spacer(modifier = Modifier.height(2.dp)) - Surface( - modifier = modifier - .fillMaxWidth(), - shape = if (actions.size - 1 == actionIndex) { - shape.copy( - topStart = flatItemSmallCornerSize, - topEnd = flatItemSmallCornerSize, + val updatedOnClick by rememberUpdatedState(action.onClick) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(actionShape) + .then(backgroundModifier) + .then( + if (action.onClick != null) { + Modifier + .clickable { + updatedOnClick?.invoke() + } + } else { + Modifier + }, ) - } else { - flatItemSmallShape - }, - color = finalBackgroundColor, - tonalElevation = animatedElevation, + .minimumInteractiveComponentSize() + .padding(contentPadding), + verticalAlignment = Alignment.CenterVertically, ) { - val updatedOnClick by rememberUpdatedState(action.onClick) - Row( - modifier = Modifier - .then( - if (action.onClick != null) { - Modifier - .clickable { - updatedOnClick?.invoke() - } - } else { - Modifier - }, - ) - .minimumInteractiveComponentSize() - .padding(contentPadding), - verticalAlignment = Alignment.CenterVertically, - ) { - FlatItemActionContent( - action = action, - compact = true, - ) - } + FlatItemActionContent( + action = action, + compact = true, + ) } } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/Scaffold.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/Scaffold.kt index 7ea0c592..835902a5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/Scaffold.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/Scaffold.kt @@ -18,14 +18,18 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.MutableWindowInsets import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState @@ -64,12 +68,16 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import com.artemchep.keyguard.platform.leDisplayCutout import com.artemchep.keyguard.platform.leIme +import com.artemchep.keyguard.platform.leNavigationBars +import com.artemchep.keyguard.platform.leStatusBars import com.artemchep.keyguard.platform.leSystemBars import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.ui.scrollbar.ColumnScrollbar import com.artemchep.keyguard.ui.scrollbar.LazyColumnScrollbar import com.artemchep.keyguard.ui.selection.SelectionBar +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.combineAlpha import dev.icerock.moko.resources.compose.stringResource import kotlinx.collections.immutable.ImmutableList @@ -95,7 +103,7 @@ fun ScaffoldLazyColumn( floatingActionButton: @Composable FabScope.() -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, pullRefreshState: PullRefreshState? = null, - containerColor: Color = MaterialTheme.colorScheme.background, + containerColor: Color = LocalSurfaceColor.current, contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = scaffoldContentWindowInsets, overlay: @Composable OverlayScope.() -> Unit = {}, @@ -186,7 +194,7 @@ fun ScaffoldColumn( floatingActionState: State = rememberUpdatedState(newValue = null), floatingActionButton: @Composable FabScope.() -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, - containerColor: Color = MaterialTheme.colorScheme.background, + containerColor: Color = LocalSurfaceColor.current, contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = scaffoldContentWindowInsets, overlay: @Composable OverlayScope.() -> Unit = {}, @@ -394,7 +402,10 @@ fun DefaultSelection( SelectionBar( title = { val text = stringResource(Res.strings.selection_n_selected, selection.count) - Text(text) + Text( + text = text, + maxLines = 2, + ) }, trailing = { val updatedOnSelectAll by rememberUpdatedState(selection.onSelectAll) @@ -473,4 +484,5 @@ data class Selection( val scaffoldContentWindowInsets @Composable get() = WindowInsets.leSystemBars + .union(WindowInsets.leDisplayCutout) .union(WindowInsets.leIme) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceColor.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceColor.kt new file mode 100644 index 00000000..abeb72e0 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceColor.kt @@ -0,0 +1,22 @@ +package com.artemchep.keyguard.ui.surface + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +val LocalSurfaceColor = staticCompositionLocalOf { + Color.White +} + +@Composable +inline fun ProvideSurfaceColor( + color: Color, + crossinline content: @Composable () -> Unit, +) { + CompositionLocalProvider( + LocalSurfaceColor provides color, + ) { + content() + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceElevation.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceElevation.kt new file mode 100644 index 00000000..c6acdb2e --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/surface/SurfaceElevation.kt @@ -0,0 +1,59 @@ +package com.artemchep.keyguard.ui.surface + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import com.artemchep.keyguard.ui.theme.combineAlpha + +data class SurfaceElevation( + val from: Float, + val to: Float, +) + +val LocalSurfaceElevation = staticCompositionLocalOf { + SurfaceElevation( + from = 0f, + to = 1f, + ) +} + +/** + * Returns a surface color that should be used for a given + * surface elevation. + */ +val SurfaceElevation.color: Color + @Composable + @ReadOnlyComposable + get() = surfaceElevationColor(to) + +@Composable +@ReadOnlyComposable +fun surfaceElevationColor(elevation: Float): Color { + when (elevation) { + 1.0f -> return MaterialTheme.colorScheme.surfaceContainerLowest + 0.75f -> return MaterialTheme.colorScheme.surfaceContainerLow + 0.5f -> return MaterialTheme.colorScheme.surfaceContainer + 0.25f -> return MaterialTheme.colorScheme.surfaceContainerHigh + } + + val min = MaterialTheme.colorScheme.surfaceContainerHigh + val max = MaterialTheme.colorScheme.surfaceContainerLowest + return max + .combineAlpha(elevation) + .compositeOver(min) +} + +val SurfaceElevation.width get() = to - from + +fun SurfaceElevation.splitLow(): SurfaceElevation { + val to = to - width / 2f + return copy(to = to) +} + +fun SurfaceElevation.splitHigh(): SurfaceElevation { + val from = from + width / 2f + return copy(from = from) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/theme/Theme.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/theme/Theme.kt index bda17852..52fd6003 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/theme/Theme.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/theme/Theme.kt @@ -288,6 +288,11 @@ fun KeyguardTheme( error = scheme._error, background = if (themeBlack && isDarkColorScheme) Color.Black else scheme.background, surface = if (themeBlack && isDarkColorScheme) Color.Black else scheme.surface, + surfaceContainerLowest = if (themeBlack && isDarkColorScheme) Color.Black else scheme.surfaceContainerLowest, + surfaceContainerLow = if (themeBlack && isDarkColorScheme) Color.Black else scheme.surfaceContainerLow, + surfaceContainer = if (themeBlack && isDarkColorScheme) Color.Black else scheme.surfaceContainer, + surfaceContainerHigh = if (themeBlack && isDarkColorScheme) scheme.surfaceContainerLow else scheme.surfaceContainerHigh, + surfaceContainerHighest = if (themeBlack && isDarkColorScheme) scheme.surfaceContainer else scheme.surfaceContainerHighest, ) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/CustomToolbar.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/CustomToolbar.kt index bbd3ba7c..0ec8e04d 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/CustomToolbar.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/CustomToolbar.kt @@ -15,12 +15,10 @@ import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarState -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -30,6 +28,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import com.artemchep.keyguard.ui.toolbar.util.ToolbarColors import kotlin.math.abs @OptIn(ExperimentalMaterial3Api::class) @@ -39,10 +38,8 @@ fun CustomToolbar( scrollBehavior: TopAppBarScrollBehavior?, content: @Composable () -> Unit, ) { - val containerColor = MaterialTheme.colorScheme.surface - val scrolledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - elevation = 3.0.dp, - ) + val containerColor = ToolbarColors.containerColor() + val scrolledContainerColor = ToolbarColors.scrolledContainerColor(containerColor) val windowInsets = TopAppBarDefaults.windowInsets // Obtain the container color from the TopAppBarColors using the `overlapFraction`. This diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/LargeToolbar.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/LargeToolbar.kt index f1674641..143a21b3 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/LargeToolbar.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/LargeToolbar.kt @@ -3,11 +3,13 @@ package com.artemchep.keyguard.ui.toolbar import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.artemchep.keyguard.platform.CurrentPlatform import com.artemchep.keyguard.platform.Platform +import com.artemchep.keyguard.ui.toolbar.util.ToolbarColors @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -28,10 +30,18 @@ fun LargeToolbar( ) return } + + val containerColor = ToolbarColors.containerColor() + val scrolledContainerColor = ToolbarColors.scrolledContainerColor(containerColor) LargeTopAppBar( title = title, modifier = modifier, navigationIcon = navigationIcon, + colors = TopAppBarDefaults.largeTopAppBarColors() + .copy( + containerColor = containerColor, + scrolledContainerColor = scrolledContainerColor, + ), actions = actions, scrollBehavior = scrollBehavior, ) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/SmallToolbar.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/SmallToolbar.kt index 3f0ab05b..bd4e538e 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/SmallToolbar.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/SmallToolbar.kt @@ -2,10 +2,18 @@ package com.artemchep.keyguard.ui.toolbar import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.unit.dp +import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevationSemi +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor +import com.artemchep.keyguard.ui.toolbar.util.ToolbarColors @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -13,13 +21,20 @@ fun SmallToolbar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable () -> Unit = {}, + containerColor: Color = ToolbarColors.containerColor(), actions: @Composable RowScope.() -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior? = null, ) { + val scrolledContainerColor = ToolbarColors.scrolledContainerColor(containerColor) TopAppBar( title = title, modifier = modifier, navigationIcon = navigationIcon, + colors = TopAppBarDefaults.topAppBarColors() + .copy( + containerColor = containerColor, + scrolledContainerColor = scrolledContainerColor, + ), actions = actions, scrollBehavior = scrollBehavior, ) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/util/ToolbarColors.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/util/ToolbarColors.kt new file mode 100644 index 00000000..7d404a1f --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/toolbar/util/ToolbarColors.kt @@ -0,0 +1,31 @@ +package com.artemchep.keyguard.ui.toolbar.util + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.isUnspecified +import androidx.compose.ui.unit.dp +import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevationSemi +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor + +object ToolbarColors { + @Composable + fun containerColor(): Color = LocalSurfaceColor.current + + @Composable + fun scrolledContainerColor( + containerColor: Color, + ): Color { + if ( + containerColor == Color.Transparent || + containerColor.isUnspecified + ) { + return containerColor + } + + val tint = MaterialTheme.colorScheme + .surfaceColorAtElevationSemi(elevation = 2.0.dp) + return tint.compositeOver(MaterialTheme.colorScheme.surfaceContainerLow) + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt b/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt index 62f525a5..3e2afd8a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt @@ -38,6 +38,7 @@ import com.artemchep.keyguard.feature.navigation.NavigationNode import com.artemchep.keyguard.feature.navigation.NavigationRouterBackHandler import com.artemchep.keyguard.platform.lifecycle.LeLifecycleState import com.artemchep.keyguard.ui.LocalComposeWindow +import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.KeyguardTheme import com.mayakapps.compose.windowstyler.WindowBackdrop import com.mayakapps.compose.windowstyler.WindowStyle @@ -194,7 +195,7 @@ fun main() { // println("event $it") // } KeyguardTheme { - val containerColor = MaterialTheme.colorScheme.background + val containerColor = MaterialTheme.colorScheme.surfaceContainerHighest val contentColor = contentColorFor(containerColor) WindowStyle( @@ -213,6 +214,7 @@ fun main() { contentColor = contentColor, ) { CompositionLocalProvider( + LocalSurfaceColor provides containerColor, LocalComposeWindow provides this.window, LocalKamelConfig provides kamelConfig, ) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abf7e3d7..e73d85cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ appVersionName = "1.0.0" # @keep appVersionCode = "2" # https://github.com/google/accompanist -accompanist = "0.32.0" +accompanist = "0.34.0" androidBillingClient = "6.1.0" # https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs androidDesugar = "2.0.4" @@ -51,7 +51,7 @@ commonsCodec = "1.16.0" # https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 commonsLang3 = "3.14.0" # https://github.com/JetBrains/compose-multiplatform -composeMultiplatform = "1.6.0-dev1357" +composeMultiplatform = "1.6.0-beta02" # https://github.com/DevSrSouza/compose-icons composeOpenIcons = "1.1.0" crashlyticsPlugin = "2.9.9"