[Compound] Bloom (#1253)
* Add `bloom` and `avatarBloom` modifiers. * Add `ConnectivityIndicatorContainer` to control the padding needed at the top. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
e6ecedf7bb
commit
41061da768
67 changed files with 913 additions and 173 deletions
1
changelog.d/1217.feature
Normal file
1
changelog.d/1217.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Implement Bloom effect modifier.
|
||||
|
|
@ -18,6 +18,9 @@ package io.element.android.features.networkmonitor.api.ui
|
|||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
|
|
@ -27,34 +30,44 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.WifiOff
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* A view that displays a connectivity indicator when the device is offline, adding a default
|
||||
* padding to make sure the status bar is not overlapped.
|
||||
*/
|
||||
@Composable
|
||||
fun ConnectivityIndicatorView(
|
||||
isOnline: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
|
||||
val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline }
|
||||
|
|
@ -78,6 +91,46 @@ fun ConnectivityIndicatorView(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A view that displays a connectivity indicator when the device is offline, passing the padding
|
||||
* needed to make sure the status bar is not overlapped to its content views.
|
||||
*/
|
||||
@Composable
|
||||
fun ConnectivityIndicatorContainer(
|
||||
isOnline: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (topPadding: Dp) -> Unit,
|
||||
) {
|
||||
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
|
||||
|
||||
val statusBarTopPadding = if (LocalInspectionMode.current) {
|
||||
// Needed to get valid UI previews
|
||||
24.dp
|
||||
} else {
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp
|
||||
}
|
||||
val target = remember(isOnline) { if (isOnline) 0.dp else statusBarTopPadding }
|
||||
val animationStateOffset by animateDpAsState(
|
||||
targetValue = target,
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessMediumLow,
|
||||
visibilityThreshold = 1.dp,
|
||||
),
|
||||
label = "insets-animation",
|
||||
)
|
||||
|
||||
content(animationStateOffset)
|
||||
|
||||
// Display the network indicator with an animation
|
||||
AnimatedVisibility(
|
||||
visibleState = isIndicatorVisible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Indicator(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Indicator(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ package io.element.android.features.roomlist.impl
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -45,7 +45,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||
import io.element.android.features.roomlist.impl.components.RequestVerificationHeader
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
|
|
@ -76,8 +76,10 @@ fun RoomListView(
|
|||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
ConnectivityIndicatorContainer(
|
||||
modifier = modifier,
|
||||
isOnline = state.hasNetworkConnection,
|
||||
) { topPadding ->
|
||||
Box {
|
||||
fun onRoomLongClicked(
|
||||
roomListRoomSummary: RoomListRoomSummary
|
||||
|
|
@ -96,6 +98,7 @@ fun RoomListView(
|
|||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
RoomListContent(
|
||||
modifier = Modifier.padding(top = topPadding),
|
||||
state = state,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
|
|
@ -111,6 +114,8 @@ fun RoomListView(
|
|||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(top = topPadding)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@
|
|||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -30,24 +33,37 @@ import androidx.compose.material3.rememberTopAppBarState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatarBloom
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.applyScaleDown
|
||||
import io.element.android.libraries.designsystem.text.roundToPx
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
|
|
@ -60,6 +76,9 @@ import io.element.android.libraries.testtags.TestTags
|
|||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.designsystem.R as CommonR
|
||||
|
||||
private val avatarBloomSize = 430.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -89,6 +108,7 @@ fun RoomListTopBar(
|
|||
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
areSearchResultsDisplayed = areSearchResultsDisplayed,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = onToggleSearch,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
|
|
@ -101,6 +121,7 @@ fun RoomListTopBar(
|
|||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onOpenSettings: () -> Unit,
|
||||
onSearchClicked: () -> Unit,
|
||||
|
|
@ -108,94 +129,139 @@ private fun DefaultRoomListTopBar(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
MediumTopAppBar(
|
||||
modifier = modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
title = {
|
||||
val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5)
|
||||
ElementTheme.typography.aliasScreenTitle
|
||||
else
|
||||
ElementTheme.typography.fontHeadingLgBold.copy(
|
||||
// Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated,
|
||||
// ensure that the font size will never be bigger than 28.dp.
|
||||
fontSize = 28.dp.applyScaleDown().toSp()
|
||||
)
|
||||
Text(
|
||||
style = fontStyle,
|
||||
text = stringResource(id = R.string.screen_roomlist_main_space_title)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
if (matrixUser != null) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
val avatarData by remember {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
Avatar(avatarData, contentDescription = stringResource(CommonStrings.common_settings))
|
||||
|
||||
// We need this to manually clip the top app bar in preview mode
|
||||
val previewAppBarHeight = if (LocalInspectionMode.current) {
|
||||
112.dp.roundToPx()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val collapsedFraction = scrollBehavior.state.collapsedFraction
|
||||
var appBarHeight by remember {
|
||||
mutableIntStateOf(previewAppBarHeight ?: 0)
|
||||
}
|
||||
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser?.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
|
||||
val statusBarPadding = with (LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
MediumTopAppBar(
|
||||
modifier = Modifier
|
||||
.onSizeChanged {
|
||||
appBarHeight = it.height
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.avatarBloom(
|
||||
avatarData = avatarData,
|
||||
background = ElementTheme.materialColors.background,
|
||||
blurSize = DpSize(avatarBloomSize, avatarBloomSize),
|
||||
offset = DpOffset(24.dp, 24.dp + statusBarPadding),
|
||||
clipToSize = if (appBarHeight > 0) DpSize(
|
||||
avatarBloomSize,
|
||||
appBarHeight.toDp()
|
||||
) else DpSize.Unspecified,
|
||||
bottomSoftEdgeAlpha = 1f - collapsedFraction,
|
||||
alpha = if (areSearchResultsDisplayed) 0f else 1f,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
),
|
||||
title = {
|
||||
val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5)
|
||||
ElementTheme.typography.aliasScreenTitle
|
||||
else
|
||||
ElementTheme.typography.fontHeadingLgBold.copy(
|
||||
// Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated,
|
||||
// ensure that the font size will never be bigger than 28.dp.
|
||||
fontSize = 28.dp.applyScaleDown().toSp()
|
||||
)
|
||||
Text(
|
||||
style = fontStyle,
|
||||
text = stringResource(id = R.string.screen_roomlist_main_space_title)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Share,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
},
|
||||
navigationIcon = {
|
||||
avatarData?.let {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = it,
|
||||
contentDescription = stringResource(CommonStrings.common_settings),
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.BugReport,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(
|
||||
resourceId = CommonR.drawable.ic_search,
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Share,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.BugReport,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.alpha(collapsedFraction)
|
||||
.align(Alignment.BottomCenter),
|
||||
color = ElementTheme.materialColors.outlineVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
@ -211,6 +277,7 @@ internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRo
|
|||
private fun DefaultRoomListTopBarPreview() {
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onSearchClicked = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,534 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.text.TextPaint
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.ClipOp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.LinearGradientShader
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.RadialGradientShader
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isSpecified
|
||||
import androidx.compose.ui.unit.toOffset
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseComposable
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import io.element.android.libraries.designsystem.R
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Default bloom configuration values.
|
||||
*/
|
||||
object BloomDefaults {
|
||||
/**
|
||||
* Number of components to use with BlurHash to generate the blur effect.
|
||||
* Larger values mean more detailed blurs.
|
||||
*/
|
||||
const val HASH_COMPONENTS = 8
|
||||
|
||||
/** Default bloom layers. */
|
||||
@Composable
|
||||
fun defaultLayers() = persistentListOf(
|
||||
// Bottom layer
|
||||
if (isSystemInDarkTheme()) {
|
||||
BloomLayer(0.5f, BlendMode.Exclusion)
|
||||
} else {
|
||||
BloomLayer(0.2f, BlendMode.Hardlight)
|
||||
},
|
||||
// Top layer
|
||||
BloomLayer(if (isSystemInDarkTheme()) 0.2f else 0.8f, BlendMode.Color),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bloom layer configuration.
|
||||
* @param alpha The alpha value to apply to the layer.
|
||||
* @param blendMode The blend mode to apply to the layer.
|
||||
*/
|
||||
data class BloomLayer(
|
||||
val alpha: Float,
|
||||
val blendMode: BlendMode,
|
||||
)
|
||||
|
||||
/**
|
||||
* Bloom effect modifier. Applies a bloom effect to the component.
|
||||
* @param hash The BlurHash to use as the bloom source.
|
||||
* @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
|
||||
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
||||
* @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used.
|
||||
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
||||
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
||||
* @param alpha The alpha value to apply to the bloom effect.
|
||||
*/
|
||||
fun Modifier.bloom(
|
||||
hash: String?,
|
||||
background: Color,
|
||||
blurSize: DpSize = DpSize.Unspecified,
|
||||
offset: DpOffset = DpOffset.Unspecified,
|
||||
clipToSize: DpSize = DpSize.Unspecified,
|
||||
layerConfiguration: ImmutableList<BloomLayer>? = null,
|
||||
bottomSoftEdgeHeight: Dp = 40.dp,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
bottomSoftEdgeAlpha: Float = 1.0f,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
alpha: Float = 1f,
|
||||
) = composed {
|
||||
val defaultLayers = BloomDefaults.defaultLayers()
|
||||
val layers = layerConfiguration ?: defaultLayers
|
||||
// Bloom only works on API 29+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
||||
if (hash == null) return@composed this
|
||||
|
||||
val hashedBitmap = remember(hash) {
|
||||
BlurHash.decode(hash, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)?.asImageBitmap()
|
||||
} ?: return@composed this
|
||||
val density = LocalDensity.current
|
||||
val pixelSize = remember(blurSize, density) { blurSize.toIntSize(density) }
|
||||
val clipToPixelSize = remember(clipToSize, density) { clipToSize.toIntSize(density) }
|
||||
val bottomSoftEdgeHeightPixels = remember(bottomSoftEdgeHeight, density) { with(density) { bottomSoftEdgeHeight.roundToPx() } }
|
||||
val isRTL = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
drawWithCache {
|
||||
val dstSize = if (pixelSize != IntSize.Zero) {
|
||||
pixelSize
|
||||
} else {
|
||||
IntSize(size.width.toInt(), size.height.toInt())
|
||||
}
|
||||
// Calculate where to place the center of the bloom effect
|
||||
val centerOffset = if (offset.isSpecified) {
|
||||
if (isRTL) {
|
||||
IntOffset(
|
||||
size.width.roundToInt() - offset.x.roundToPx(),
|
||||
size.height.roundToInt() - offset.y.roundToPx(),
|
||||
)
|
||||
} else {
|
||||
IntOffset(
|
||||
offset.x.roundToPx(),
|
||||
offset.y.roundToPx(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IntOffset(
|
||||
size.center.x.toInt(),
|
||||
size.center.y.toInt(),
|
||||
)
|
||||
}
|
||||
// Calculate the offset to draw the different layers and apply clipping
|
||||
// This offset is applied to place the top left corner of the bloom effect
|
||||
val layersOffset = if (offset.isSpecified) {
|
||||
// Offsets the layers so the center of the bloom effect is at the provided offset value
|
||||
IntOffset(
|
||||
centerOffset.x - dstSize.width / 2,
|
||||
centerOffset.y - dstSize.height / 2,
|
||||
)
|
||||
} else {
|
||||
// Places the layers at the center of the component
|
||||
IntOffset.Zero
|
||||
}
|
||||
val radius = max(dstSize.width, dstSize.height).toFloat() / 2
|
||||
val circularGradientShader = RadialGradientShader(
|
||||
centerOffset.toOffset(),
|
||||
radius,
|
||||
listOf(Color.Red, Color.Transparent),
|
||||
listOf(0f, 1f)
|
||||
)
|
||||
val circularGradientBrush = ShaderBrush(circularGradientShader)
|
||||
val bottomEdgeGradient = LinearGradientShader(
|
||||
from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(),
|
||||
to = IntOffset(0, clipToPixelSize.height).toOffset(),
|
||||
listOf(Color.Transparent, background),
|
||||
listOf(0f, 1f)
|
||||
)
|
||||
val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient)
|
||||
onDrawBehind {
|
||||
if (dstSize != IntSize.Zero) {
|
||||
val circleClipPath = Path().apply {
|
||||
addOval(Rect(centerOffset.toOffset(), radius - 1))
|
||||
}
|
||||
// Clip the external radius of bloom gradient too, otherwise we have a 1px border
|
||||
clipPath(circleClipPath, clipOp = ClipOp.Intersect) {
|
||||
// Draw the bloom layers
|
||||
drawWithLayer {
|
||||
// Clip rect to the provided size if needed
|
||||
if (clipToPixelSize != IntSize.Zero) {
|
||||
drawContext.canvas.clipRect(Rect(Offset.Zero, clipToPixelSize.toSize()), ClipOp.Intersect)
|
||||
}
|
||||
// Draw background color for blending
|
||||
drawRect(background, size = pixelSize.toSize())
|
||||
// Draw layers
|
||||
for (layer in layers) {
|
||||
drawImage(
|
||||
hashedBitmap,
|
||||
srcSize = IntSize(BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS),
|
||||
dstSize = dstSize,
|
||||
dstOffset = layersOffset,
|
||||
alpha = layer.alpha * alpha,
|
||||
blendMode = layer.blendMode,
|
||||
)
|
||||
}
|
||||
// Mask the layers erasing the outer radius using the gradient brush
|
||||
drawCircle(
|
||||
circularGradientBrush,
|
||||
radius,
|
||||
centerOffset.toOffset(),
|
||||
blendMode = BlendMode.DstIn
|
||||
)
|
||||
}
|
||||
}
|
||||
// Draw the bottom soft edge
|
||||
drawRect(
|
||||
bottomEdgeGradientBrush,
|
||||
topLeft = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeight.roundToPx()).toOffset(),
|
||||
size = IntSize(pixelSize.width, bottomSoftEdgeHeight.roundToPx()).toSize(),
|
||||
alpha = bottomSoftEdgeAlpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bloom effect modifier for avatars. Applies a bloom effect to the component.
|
||||
* @param avatarData The avatar data to use as the bloom source.
|
||||
* If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used. If `null` is passed, no bloom effect will be applied.
|
||||
* @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
|
||||
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
||||
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
||||
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
||||
* @param alpha The alpha value to apply to the bloom effect.
|
||||
*/
|
||||
fun Modifier.avatarBloom(
|
||||
avatarData: AvatarData?,
|
||||
background: Color,
|
||||
blurSize: DpSize = DpSize.Unspecified,
|
||||
offset: DpOffset = DpOffset.Unspecified,
|
||||
clipToSize: DpSize = DpSize.Unspecified,
|
||||
bottomSoftEdgeHeight: Dp = 40.dp,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
bottomSoftEdgeAlpha: Float = 1.0f,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
alpha: Float = 1f,
|
||||
) = composed {
|
||||
// Bloom only works on API 29+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
||||
avatarData ?: return@composed this
|
||||
|
||||
if (avatarData.url != null) {
|
||||
// Request the avatar contents to use as the bloom source
|
||||
val painter = rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current)
|
||||
.data(avatarData)
|
||||
// Needed to be able to read pixels from the Bitmap for the hash
|
||||
.allowHardware(false)
|
||||
.build()
|
||||
)
|
||||
var blurHash by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(avatarData) {
|
||||
val drawable = painter.imageLoader.execute(painter.request).drawable ?: return@LaunchedEffect
|
||||
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@LaunchedEffect
|
||||
blurHash = BlurHash.encode(bitmap, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
||||
}
|
||||
|
||||
bloom(
|
||||
hash = blurHash,
|
||||
background = background,
|
||||
blurSize = blurSize,
|
||||
offset = offset,
|
||||
clipToSize = clipToSize,
|
||||
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
|
||||
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
|
||||
alpha = alpha,
|
||||
)
|
||||
} else {
|
||||
// There is no URL so we'll generate an avatar with the initials and use that as the bloom source
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
||||
val initialsBitmap = initialsBitmap(
|
||||
width = avatarData.size.dp,
|
||||
height = avatarData.size.dp,
|
||||
text = avatarData.initial,
|
||||
textColor = avatarColors.foreground,
|
||||
backgroundColor = avatarColors.background,
|
||||
)
|
||||
val hash = remember(avatarData, avatarColors) {
|
||||
BlurHash.encode(initialsBitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
||||
}
|
||||
bloom(
|
||||
hash = hash,
|
||||
background = background,
|
||||
blurSize = blurSize,
|
||||
offset = offset,
|
||||
clipToSize = clipToSize,
|
||||
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
|
||||
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
|
||||
alpha = alpha,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Used to create a Bitmap version of the initials avatar
|
||||
@Composable
|
||||
private fun initialsBitmap(
|
||||
text: String,
|
||||
backgroundColor: Color,
|
||||
textColor: Color,
|
||||
width: Dp = 32.dp,
|
||||
height: Dp = 32.dp,
|
||||
): ImageBitmap = with(LocalDensity.current) {
|
||||
val backgroundPaint = remember(backgroundColor) {
|
||||
Paint().also { it.color = backgroundColor }
|
||||
}
|
||||
val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current
|
||||
val fontSize = remember { height.toSp() / 2 }
|
||||
val typeface: Typeface = remember(resolver) {
|
||||
resolver.resolve(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Normal,
|
||||
)
|
||||
}.value as Typeface
|
||||
val textPaint = remember(textColor, typeface) {
|
||||
TextPaint().apply {
|
||||
color = textColor.toArgb()
|
||||
textSize = fontSize.toPx()
|
||||
this.typeface = typeface
|
||||
}
|
||||
}
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val result = remember(text) { textMeasurer.measure(text, TextStyle.Default.copy(fontSize = fontSize)) }
|
||||
val centerPx = remember(width, height) { IntOffset(width.roundToPx() / 2, height.roundToPx() / 2) }
|
||||
remember(text, width, height, backgroundColor, textColor) {
|
||||
val bitmap = Bitmap.createBitmap(width.roundToPx(), height.roundToPx(), Bitmap.Config.ARGB_8888).asImageBitmap()
|
||||
androidx.compose.ui.graphics.Canvas(bitmap).also { canvas ->
|
||||
canvas.drawCircle(centerPx.toOffset(), width.toPx() / 2, backgroundPaint)
|
||||
canvas.nativeCanvas.drawText(text, centerPx.x.toFloat() - result.size.width/2, centerPx.y * 2f - result.size.height/2 - 4, textPaint)
|
||||
}
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// Translates DP sizes into pixel sizes, taking into account unspecified values
|
||||
private fun DpSize.toIntSize(density: Density) = with(density) {
|
||||
if (isSpecified) {
|
||||
IntSize(width.roundToPx(), height.roundToPx())
|
||||
} else {
|
||||
IntSize.Zero
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to draw to a canvas using layers. This allows us to apply clipping to those layers only.
|
||||
*/
|
||||
fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
|
||||
with(drawContext.canvas.nativeCanvas) {
|
||||
val checkPoint = saveLayer(null, null)
|
||||
block()
|
||||
restoreToCount(checkPoint)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@DayNightPreviews
|
||||
@ShowkaseComposable(group = PreviewGroup.Bloom)
|
||||
@Composable
|
||||
internal fun BloomPreview() {
|
||||
val blurhash = "eePn{tI?xExEja}ooKWWodjtNJoKR,j@a|sBWpS3WDbGazoKWWWWj@"
|
||||
var topAppBarHeight by remember { mutableIntStateOf(-1) }
|
||||
val topAppBarState = rememberTopAppBarState()
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
|
||||
ElementPreview {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Box {
|
||||
MediumTopAppBar(
|
||||
modifier = Modifier
|
||||
.onSizeChanged { size ->
|
||||
topAppBarHeight = size.height
|
||||
}
|
||||
.bloom(
|
||||
hash = blurhash,
|
||||
background = ElementTheme.materialColors.background,
|
||||
blurSize = DpSize(430.dp, 430.dp),
|
||||
offset = DpOffset(24.dp, 24.dp),
|
||||
clipToSize = if (topAppBarHeight > 0) DpSize(430.dp, topAppBarHeight.toDp()) else DpSize.Zero,
|
||||
),
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Black.copy(alpha = 0.05f),
|
||||
),
|
||||
navigationIcon = {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(id = R.drawable.sample_avatar),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(imageVector = Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text("Title")
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
repeat(20) {
|
||||
Text("Content", modifier = Modifier.padding(vertical = 20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InitialsColorStateProvider : PreviewParameterProvider<Int> {
|
||||
override val values: Sequence<Int>
|
||||
get() = sequenceOf(0, 1, 2, 3, 4, 5, 6, 7)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
@ShowkaseComposable(group = PreviewGroup.Bloom)
|
||||
internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider::class) color: Int) {
|
||||
ElementPreview {
|
||||
val avatarColors = AvatarColorsProvider.provide("$color", ElementTheme.isLightTheme)
|
||||
val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground)
|
||||
val hash = BlurHash.encode(bitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
||||
Box(
|
||||
modifier = Modifier.size(256.dp)
|
||||
.bloom(
|
||||
hash = hash,
|
||||
background = ElementTheme.materialColors.background,
|
||||
blurSize = DpSize(256.dp, 256.dp),
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape),
|
||||
painter = BitmapPainter(bitmap),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
|
||||
enum class AvatarSize(val dp: Dp) {
|
||||
CurrentUserTopBar(28.dp),
|
||||
CurrentUserTopBar(32.dp),
|
||||
|
||||
RoomHeader(96.dp),
|
||||
RoomListItem(52.dp),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.preview
|
|||
object PreviewGroup {
|
||||
const val AppBars = "App Bars"
|
||||
const val Avatars = "Avatars"
|
||||
const val Bloom = "Bloom"
|
||||
const val BottomSheets = "Bottom Sheets"
|
||||
const val Buttons = "Buttons"
|
||||
const val DateTimePickers = "DateTime pickers"
|
||||
|
|
|
|||
25
libraries/designsystem/src/main/res/drawable/ic_search.xml
Normal file
25
libraries/designsystem/src/main/res/drawable/ic_search.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!--
|
||||
~ Copyright (c) 2023 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.05 16.46C13.79 17.43 12.2 18 10.5 18 6.36 18 3 14.64 3 10.5 3 6.36 6.36 3 10.5 3c4.14 0 7.5 3.36 7.5 7.5 0 1.71-0.57 3.29-1.54 4.55l3.25 3.24c0.39 0.4 0.39 1.03 0 1.42-0.4 0.39-1.03 0.39-1.42 0l-3.24-3.25ZM16 10.5C16 7.46 13.54 5 10.5 5S5 7.46 5 10.5 7.46 16 10.5 16s5.5-2.46 5.5-5.5Z"/>
|
||||
</vector>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8421be9fe6518a4a610e561692352c28c63b1e588fae4f7617f6be94439852f2
|
||||
size 45078
|
||||
oid sha256:ee62a7f77e591d238878e72e3f2fbe489e8c4e8c6a5c121b16e120ad5c43836d
|
||||
size 44185
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c30a6c0c7169044216a02dde5605d59fba9307499855ffc1fa0d3fcd1650f5e
|
||||
size 43291
|
||||
oid sha256:d4bdf3944d30980e9bfd7cfffbabe2dac9861748cc3006e751bfd5cbf59c86db
|
||||
size 42475
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c40273c36eb1479f284e75fa91d4a75b1ae97edd0242dda37a2d4a8f10394928
|
||||
size 138700
|
||||
oid sha256:6c0485788c8776536a01613058ab001bad02a67519cf0cb4892505c7c3715fea
|
||||
size 144681
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7c47c713c74766c39d3b349d9e421ea588261e043105a5475cd8e68af782806
|
||||
size 185325
|
||||
oid sha256:b4a259f036e555fdc52ecc8f0c9578dae70ed677235fbfd344bc31cbabc76be5
|
||||
size 181478
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d5479f3a6ea138b00c8f6a376cc4a67984385a087fb32bcdb764b2996e89402
|
||||
size 137137
|
||||
oid sha256:d5966cae164fed6a3a12c1e1dd940ae7b0601e4ba69d11f49d18740bdab8c520
|
||||
size 130130
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c40273c36eb1479f284e75fa91d4a75b1ae97edd0242dda37a2d4a8f10394928
|
||||
size 138700
|
||||
oid sha256:6c0485788c8776536a01613058ab001bad02a67519cf0cb4892505c7c3715fea
|
||||
size 144681
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7c47c713c74766c39d3b349d9e421ea588261e043105a5475cd8e68af782806
|
||||
size 185325
|
||||
oid sha256:b4a259f036e555fdc52ecc8f0c9578dae70ed677235fbfd344bc31cbabc76be5
|
||||
size 181478
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d5479f3a6ea138b00c8f6a376cc4a67984385a087fb32bcdb764b2996e89402
|
||||
size 137137
|
||||
oid sha256:b8cb21e08faf31779111d78c2be1e4e68e6f3bc994b16c108bd654f13d39fa02
|
||||
size 130130
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06617cda0f93ce0ea26b2a77a243371cdaf4f9a2956d8159ecb7196f7f6fe082
|
||||
size 139282
|
||||
oid sha256:f2782edd31f68b7aa27e0c35b29d7c84574326cb56c65394738357479d408f5f
|
||||
size 144667
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55d2d11ba729a68b1b62e9a69c3308594839fdb3612ff3db59850f0b346c28da
|
||||
size 186118
|
||||
oid sha256:cd352f748e3fd08225aa714181fdc18d53e91a8c62f8e5e860b67ff5aaeb7b8e
|
||||
size 181969
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62c57a45c2cb1aac23fa0c3af44f90a5d7b9a1e1626ec49eb94a2c126f53f96d
|
||||
size 137535
|
||||
oid sha256:43da903ec671dd1cb591396306ce8c057432ffa8c1bfce7a509f28576e82b530
|
||||
size 130337
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06617cda0f93ce0ea26b2a77a243371cdaf4f9a2956d8159ecb7196f7f6fe082
|
||||
size 139282
|
||||
oid sha256:f2782edd31f68b7aa27e0c35b29d7c84574326cb56c65394738357479d408f5f
|
||||
size 144667
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55d2d11ba729a68b1b62e9a69c3308594839fdb3612ff3db59850f0b346c28da
|
||||
size 186118
|
||||
oid sha256:cd352f748e3fd08225aa714181fdc18d53e91a8c62f8e5e860b67ff5aaeb7b8e
|
||||
size 181969
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62c57a45c2cb1aac23fa0c3af44f90a5d7b9a1e1626ec49eb94a2c126f53f96d
|
||||
size 137535
|
||||
oid sha256:8141eaa257454258d2926df5ee970daf5ee165b774e4b948256b90e22a388d4c
|
||||
size 130337
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0ad6895582183b697732ef032f89a6a0cad32106c9bbf30925c390dcab65ee6
|
||||
size 151752
|
||||
oid sha256:f95fbf1b51966c1c6b7b149c771f33400455d3c999b505e1aa65f0e3eb41c0ef
|
||||
size 140928
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:676b62ab782652c45a7267aac5df12943fd181bea48964d28dce664884ae4182
|
||||
size 156619
|
||||
oid sha256:e8d522eb99079792d010e053d488f78a95e91f5a1b614b9d512bd005f0df81cf
|
||||
size 146976
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b991a34a137d82ff4dedf48316a05ebbc008233984b11af70e64889a5fb5eda8
|
||||
size 127438
|
||||
oid sha256:eebf95f938f483da9b087eda86ae4a1baac594bc4b6f1f893d869b759679526c
|
||||
size 123267
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43b6514716d18382c8962520928d9d2ff6d0f665b2471e8a24e5d2e44a25c88c
|
||||
size 132295
|
||||
oid sha256:4875df98092a8ef8bd1c2fda8c4a038ae2830b9ffa57e4ab71859523f97c26c9
|
||||
size 128062
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0d7bc773e70c57226840b30675f14a7fd99026100d61f48eeab64fb55d43f7c6
|
||||
size 230426
|
||||
oid sha256:30119e8fcb65e8cbf058191268f0d70633a62b160b9f49f71879272d0d076537
|
||||
size 241818
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:87f93b06f658352e7d97617c28dc0a4bd7454263fd402abfc2676f548c41d6b2
|
||||
size 231314
|
||||
oid sha256:73ca2b85c0f1f8f13cd3a0aae9d394116a460aa2c511ca1f2672a356eee496d2
|
||||
size 241553
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f13fbb9bc776fa230b69c41e3faab659c52083d4e4f737f84b472c966f42c8dd
|
||||
size 229746
|
||||
oid sha256:ff7953fabf75b346e61bd4335f3753331b498dbe5f17f9af3aad8a6a1ce051cd
|
||||
size 239490
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d312c80758e940b368e0b9eb2d41efb55c158691164bfde3456d7540080e46a9
|
||||
size 230651
|
||||
oid sha256:e262b1128df681c9a98bbefe426a891d02e0500646319f2e1756c1b5c93f2d23
|
||||
size 239303
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bbfb4b373449d3ce3ff06ad67e8c4ecfcbb0ec8d0bcb400d3c010952d11e8ad5
|
||||
size 10480
|
||||
oid sha256:51931b01aa27677c8d29a79bf92f4e8b48fc2be05e80fdabedfb9026dcbd8899
|
||||
size 31634
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d0cc556a6c431fc0f10958a4760fe63d0dd3d7f976ce3507643308c50f37c2e
|
||||
size 10381
|
||||
oid sha256:5441c89fc077720b52479d4daf1ebd5fe5b98b63f35141a550dac228fe1c649f
|
||||
size 10617
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66bf2e3b890ab75c77f1a9e4e80a374cbb5af35e72b8f53c86424c56f1a9f524
|
||||
size 36582
|
||||
oid sha256:9156202a9f621b75cff3ca45aac33d72a938620049948828fc34aab3b9e0980d
|
||||
size 55382
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1aff1b446de80f7535a71fbc77d7955a679ff2846bb240b70f2d410114f29b16
|
||||
size 59749
|
||||
oid sha256:0c7ee3acbf4653e49633e68f896fe0f5f73591c2cbc889204cc244b319996e0d
|
||||
size 78348
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66bf2e3b890ab75c77f1a9e4e80a374cbb5af35e72b8f53c86424c56f1a9f524
|
||||
size 36582
|
||||
oid sha256:9156202a9f621b75cff3ca45aac33d72a938620049948828fc34aab3b9e0980d
|
||||
size 55382
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0fd240006e3b08b081fccdcc29cb8911fbde7f923c4f8207de817ea6a42d3d6f
|
||||
size 38475
|
||||
oid sha256:55800431dfe4453fa3c11f97bfae53ceaf95f1c18b72bcc8620b8c3295b26b27
|
||||
size 56388
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:77d78ad2cabaac211517c129a42d8164b02ba2cc5b75a13f27f2ae113b2b9f06
|
||||
size 37924
|
||||
oid sha256:b2d288f46eb76b8a71a8c1a2ec60ba7c3b8b1bf2a92be9d8daf33c06b3a0f126
|
||||
size 56593
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9701408630ece8c6178e792920688eb41a8fe1a4621c45f8cf34338f134dded2
|
||||
size 38290
|
||||
oid sha256:c0e5bc6abe96bff50960c5ace2c4eccd5aabdd9f3ccd015fae867b4c3bc2b423
|
||||
size 56902
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56f0ecce5efe141fa2181e2c16dc654e79c4de43cb11b18004b03ce2ddad991d
|
||||
size 38801
|
||||
oid sha256:1f385e70f3a8bda7c0c2f7c090ba665575239299472c0fbd62f2821783ceb065
|
||||
size 39016
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f9d7be89279d30e82a689654eac67137678ca4eb079e80372011580bcf0e9e22
|
||||
size 62757
|
||||
oid sha256:4bdb22d629e4bc3273aac4944802d1c91b43065858f4f4cc90052bd78ae6dd89
|
||||
size 62998
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56f0ecce5efe141fa2181e2c16dc654e79c4de43cb11b18004b03ce2ddad991d
|
||||
size 38801
|
||||
oid sha256:1f385e70f3a8bda7c0c2f7c090ba665575239299472c0fbd62f2821783ceb065
|
||||
size 39016
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:db76df516d6c7a4bff5b830e423145c448b076846b8b7f75032be6f65585cd31
|
||||
size 40846
|
||||
oid sha256:8ec1d951c96737ff8c5b401ef7c3af944f76be69348559401e40a12f60d5d459
|
||||
size 40807
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:405842db16444b958899fe174ca1c0157c0c6384f8ae0be37be104de8613f140
|
||||
size 40126
|
||||
oid sha256:ef7d9c31a72334294e93c67af1ca068ddff0062ae909ee8b682f0117c5c0ab6c
|
||||
size 40331
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:658ee0f231146446810d7c0d09d280148d68475d2411c59cf0ac70caabf4e3ec
|
||||
size 40496
|
||||
oid sha256:54ebfc606c04f37ed72edf5e9168a55628b59e69ea8a03cc3925cf25ca807195
|
||||
size 40695
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0a2a737eb81f74c50efb44c03ac9eb528c38ff393dc1a32efa0c148e27da4668
|
||||
size 17880
|
||||
oid sha256:877bec872499d1a1f15b50c95c507be6233a2ea121cf3429e255b3bde66c1d7b
|
||||
size 18331
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff929a3cfa47f93176817cc844b9ef2f5ff6146f016606515efe858fadd00247
|
||||
size 17199
|
||||
oid sha256:84da3f320738e96b2c90724b278b0b2538e55f4089753f10e4913f7f759fd1c3
|
||||
size 17533
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d9b589643f99328d44a38a263b50c4447df7104a6fc0494dd80bbb2920752cb1
|
||||
size 19519
|
||||
oid sha256:ce86a5fe5ee55b411bd1f76b59e12baafb0cbc963251c9e58d047b1a2a200331
|
||||
size 20274
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3bd86a694456603dce070a7bef0646e553cefdb365f81c9d835b28d6b2521950
|
||||
size 71026
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67be26f120cbce266651737206a5f444b9a0605a4528cea30971f4629b340501
|
||||
size 85653
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7142834262eb7db1909a5a48621979cb1a2ee00bc3b70de08e90523f53083e2d
|
||||
size 85294
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3424334cf7831701633ebe3b865a5696d6843c41acc372a6dcddadaaa371160f
|
||||
size 83088
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e5583fd71bfe7ae518b4b90c3d3b00f1a65829dc015c6384ddd1a560ca77bdb
|
||||
size 88567
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88902f2d5512e22e1f5cd50d0628c7f968a0ee9e88b16a9d0d55fd2e11aefcba
|
||||
size 85255
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a55cef72cf73e76104b74d55fe0abe92efa60e094116d871686fc4f525ca3649
|
||||
size 84298
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:70919d061d76126b09642e2d0f1b41b3d0b6a4cff68f68c6bd2e43d141732dd3
|
||||
size 87805
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b62cd8043e21bed0a323d657f79605aa9a0ad3fae632387d175fdd764baf8ce
|
||||
size 81507
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aafacbe3b1d14fdb11144d65148a24400fe964c4cd880796f8cc829abf37ae05
|
||||
size 5294
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:30bb9d0a709868ff29803e45265069aad6e75ea2e65cdd8524b83a490a666831
|
||||
size 5272
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c56746d71fa25bf610f041b6c72e4559282407808bedbcdcc3c71a4497b3244
|
||||
size 5417
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8be31e6a338f76e28550062911766dab5e4744e3e514f1348636100885ac1fe7
|
||||
size 5310
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:813856d48fdb7b168b5cd662113a54dad9286b5f4191379471866ef6eda55e84
|
||||
size 5336
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83adb2ece84370f3f7249765b391ca30dcb5ba9a2ceb04872bbb7f66101890a7
|
||||
size 5340
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a71d6d556b72e3be69f0a9902771066259123f002f7fd35652a64aa8c26c9eea
|
||||
size 5256
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:667ecdc1069d2111044ca8f55f2fe12ff7aff84529b65c89e1c0223d841fb455
|
||||
size 5465
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c452593117cb003aca6d7d3c547b96a879c1bb10a169926c507c57a2abdc9b1
|
||||
size 77557
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e65e15c721a939ed700719cacbdee57e1ccb89d984e88bdb5ed772c4b583472a
|
||||
size 81494
|
||||
oid sha256:700c12595cb3df0b342b36816dd336424ae1a94245874cb1e989528e89964336
|
||||
size 79384
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49f6d3b7f93008640abf30a5a65ddca6845e16343ef97c3450faf9dd6d9b9cec
|
||||
size 78788
|
||||
oid sha256:9f562083dcd0746eab20db445fce2836628faf5b57077dbb63ddbc32a479a9a0
|
||||
size 76138
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue