feat: Material3 v1.2 with tone-based surfaces

This commit is contained in:
Artem Chepurnoy 2024-02-11 22:08:07 +02:00
parent 80318722c5
commit 93bac97fb8
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
30 changed files with 1333 additions and 591 deletions

View File

@ -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)

View File

@ -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<AttachmentsState>,
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,
)

View File

@ -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

View File

@ -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<GeneratorState>,
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<GeneratorState>,
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<GeneratorState>,
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,

View File

@ -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<NavigationEntry>,
routes: List<Rail>,
routes: ImmutableList<Rail>,
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<GetNavLabel>()
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<NavigationEntry>,
backStack: ImmutableList<NavigationEntry>,
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<NavigationEntry>,
backStack: ImmutableList<NavigationEntry>,
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<NavigationEntry>,
route: Route,

View File

@ -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,

View File

@ -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.

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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 = {

View File

@ -62,6 +62,7 @@ private data class Foo(
fun NavigationNode(
entries: PersistentList<NavigationEntry>,
offset: Int = 0,
modifier: Modifier = Modifier,
) {
val getNavAnimation by rememberInstance<GetNavAnimation>()
@ -79,7 +80,9 @@ fun NavigationNode(
val logicalStack = LocalNavigationNodeLogicalStack.current
val visualStack = LocalNavigationNodeVisualStack.current
Box {
Box(
modifier = modifier,
) {
//
// Draw screen stack
//

View File

@ -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,
)
}
}
}
}

View File

@ -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(

View File

@ -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 },

View File

@ -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))
}

View File

@ -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<GetNavAnimation>()
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,
)
}
}
}
}

View File

@ -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,
)
}
}
}
}
}
}

View File

@ -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

View File

@ -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,
)
}
}
}

View File

@ -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<FabState?> = 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)

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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,
)
}

View File

@ -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

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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)
}
}

View File

@ -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,
) {

View File

@ -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"