1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-3590 Vault 2.0 | Empty state (#2488)

This commit is contained in:
Konstantin Ivanov 2025-06-02 15:17:39 +02:00 committed by GitHub
parent ae0ed5941f
commit 4582af43bf
Signed by: github
GPG key ID: B5690EEEBB952194
4 changed files with 285 additions and 133 deletions

View file

@ -0,0 +1,69 @@
package com.anytypeio.anytype.ui.vault
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
@Composable
fun VaultEmptyState(
onCreateSpaceClicked: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_vault_create_space),
contentDescription = "Empty state icon",
modifier = Modifier.wrapContentSize()
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(id = R.string.vault_empty_state_text),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
ButtonSecondary(
onClick = onCreateSpaceClicked,
modifier = Modifier,
size = ButtonSize.Small,
text = stringResource(id = R.string.create_space),
)
}
}
@DefaultPreviews
@Composable
fun VaultEmptyStatePreview() {
VaultEmptyState(
onCreateSpaceClicked = {}
)
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@ -25,6 +26,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -39,11 +41,13 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import coil3.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -60,8 +64,11 @@ import com.anytypeio.anytype.core_ui.foundation.util.dragContainer
import com.anytypeio.anytype.core_ui.foundation.util.rememberDragDropState
import com.anytypeio.anytype.core_ui.views.AvatarTitle
import com.anytypeio.anytype.core_ui.views.BodySemiBold
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.views.animations.conditionalBackground
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.presentation.editor.cover.CoverGradient
import com.anytypeio.anytype.presentation.profile.AccountProfile
@ -71,9 +78,6 @@ import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.vault.VaultViewModel.VaultSpaceView
import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import androidx.compose.foundation.layout.Row
import com.anytypeio.anytype.core_ui.views.Caption1Regular
@Composable
@ -92,6 +96,12 @@ fun VaultScreen(
spaceList = spaces
val lazyListState = rememberLazyListState()
val isScrolled = remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex > 0 || lazyListState.firstVisibleItemScrollOffset > 0
}
}
val dragDropState = rememberDragDropState(
lazyListState = lazyListState,
onDragEnd = {
@ -117,68 +127,65 @@ fun VaultScreen(
Modifier
)
) {
VaultScreenToolbar(
profile = profile,
onPlusClicked = onCreateSpaceClicked,
onSettingsClicked = onSettingsClicked,
spaceCountLimitReached = spaces.size >= SelectSpaceViewModel.MAX_SPACE_COUNT,
isScrolled = isScrolled.value
)
VaultScreenToolbar(
profile = profile,
onPlusClicked = onCreateSpaceClicked,
onSettingsClicked = onSettingsClicked,
spaceCountLimitReached = spaces.size >= SelectSpaceViewModel.MAX_SPACE_COUNT
)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(top = 48.dp)
.dragContainer(dragDropState)
,
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(
items = spaceList,
key = { _, item ->
item.space.id
}
) { idx, item ->
if (idx == 0) {
Spacer(modifier = Modifier.height(4.dp))
}
DraggableItem(dragDropState = dragDropState, index = idx) {
if (item.space.isLoading) {
LoadingSpaceCard()
} else {
VaultSpaceCard(
title = item.space.name.orEmpty(),
subtitle = when (item.space.spaceAccessType) {
SpaceAccessType.PRIVATE -> stringResource(id = R.string.space_type_private_space)
SpaceAccessType.DEFAULT -> stringResource(id = R.string.space_type_default_space)
SpaceAccessType.SHARED -> stringResource(id = R.string.space_type_shared_space)
else -> EMPTY_STRING_VALUE
},
wallpaper = item.wallpaper,
onCardClicked = { onSpaceClicked(item) },
icon = item.icon,
unreadMessageCount = item.unreadMessageCount,
unreadMentionCount = item.unreadMentionCount
)
}
}
if (idx == spaces.lastIndex && spaces.size < SelectSpaceViewModel.MAX_SPACE_COUNT) {
VaultSpaceAddCard(
onCreateSpaceClicked = onCreateSpaceClicked
)
Spacer(modifier = Modifier.height(40.dp))
}
}
if (spaceList.isEmpty()) {
item {
VaultSpaceAddCard(
onCreateSpaceClicked = onCreateSpaceClicked
)
Spacer(modifier = Modifier.height(40.dp))
}
}
}
if (spaces.isEmpty()) {
VaultEmptyState(
onCreateSpaceClicked = onCreateSpaceClicked
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(top = 48.dp)
.dragContainer(dragDropState),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(
items = spaceList,
key = { _, item ->
item.space.id
}
) { idx, item ->
if (idx == 0) {
Spacer(modifier = Modifier.height(4.dp))
}
DraggableItem(dragDropState = dragDropState, index = idx) {
if (item.space.isLoading) {
LoadingSpaceCard()
} else {
VaultSpaceCard(
title = item.space.name.orEmpty(),
subtitle = when (item.space.spaceAccessType) {
SpaceAccessType.PRIVATE -> stringResource(id = R.string.space_type_private_space)
SpaceAccessType.DEFAULT -> stringResource(id = R.string.space_type_default_space)
SpaceAccessType.SHARED -> stringResource(id = R.string.space_type_shared_space)
else -> EMPTY_STRING_VALUE
},
wallpaper = item.wallpaper,
onCardClicked = { onSpaceClicked(item) },
icon = item.icon,
unreadMessageCount = item.unreadMessageCount,
unreadMentionCount = item.unreadMentionCount
)
}
}
if (idx == spaces.lastIndex && spaces.size < SelectSpaceViewModel.MAX_SPACE_COUNT) {
VaultSpaceAddCard(
onCreateSpaceClicked = onCreateSpaceClicked
)
Spacer(modifier = Modifier.height(40.dp))
}
}
}
}
}
}
@ -189,81 +196,109 @@ fun VaultScreenToolbar(
profile: AccountProfile,
spaceCountLimitReached: Boolean = false,
onPlusClicked: () -> Unit,
onSettingsClicked: () -> Unit
onSettingsClicked: () -> Unit,
isScrolled: Boolean = false
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = stringResource(R.string.vault_my_spaces),
style = Title1,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.align(Alignment.Center)
)
when(profile) {
is AccountProfile.Data -> {
Box(
Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp)
.size(32.dp)
.noRippleClickable {
onSettingsClicked()
}
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.conditionalBackground(
condition = isScrolled,
) {
when(val icon = profile.icon) {
is ProfileIconView.Image -> {
GlideImage(
model = icon.url,
contentDescription = "Custom image profile",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
)
}
else -> {
val nameFirstChar = if (profile.name.isEmpty()) {
stringResource(id = com.anytypeio.anytype.ui_settings.R.string.account_default_name)
} else {
profile.name.first().uppercaseChar().toString()
background(
color = colorResource(R.color.navigation_panel),
)
}
.height(44.dp)
) {
if (isScrolled) {
Text(
text = stringResource(R.string.vault_my_spaces),
style = Title1,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.align(Alignment.Center)
)
}
when (profile) {
is AccountProfile.Data -> {
Box(
Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp)
.size(28.dp)
.noRippleClickable {
onSettingsClicked()
}
Box(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.background(colorResource(id = com.anytypeio.anytype.ui_settings.R.color.text_tertiary))
) {
Text(
text = nameFirstChar,
style = AvatarTitle.copy(
fontSize = 20.sp
),
color = colorResource(id = R.color.text_white),
modifier = Modifier.align(Alignment.Center)
) {
when (val icon = profile.icon) {
is ProfileIconView.Image -> {
Image(
painter = rememberAsyncImagePainter(icon.url),
contentDescription = "Custom image profile",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
)
}
else -> {
val nameFirstChar = if (profile.name.isEmpty()) {
stringResource(id = com.anytypeio.anytype.ui_settings.R.string.account_default_name)
} else {
profile.name.first().uppercaseChar().toString()
}
Box(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.background(colorResource(id = com.anytypeio.anytype.ui_settings.R.color.text_tertiary))
) {
Text(
text = nameFirstChar,
style = AvatarTitle.copy(
fontSize = 20.sp
),
color = colorResource(id = R.color.text_white),
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
}
AccountProfile.Idle -> {
// Draw nothing
}
}
AccountProfile.Idle -> {
// Draw nothing
if (!spaceCountLimitReached) {
Image(
painter = painterResource(id = R.drawable.ic_plus_18),
contentDescription = "Plus button",
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 16.dp)
.size(32.dp)
.noRippleClickable {
onPlusClicked()
}
)
}
}
if (!spaceCountLimitReached) {
Image(
painter = painterResource(id = R.drawable.ic_vault_top_toolbar_plus),
contentDescription = "Plus button",
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 16.dp)
.noRippleClickable {
onPlusClicked()
}
if (!isScrolled) {
Text(
modifier = Modifier.padding(top = 3.dp, bottom = 8.dp, start = 16.dp),
text = stringResource(R.string.vault_my_spaces),
style = HeadlineTitle.copy(
fontSize = 34.sp
),
color = colorResource(id = R.color.text_primary),
textAlign = TextAlign.Center
)
}
}
@ -300,6 +335,7 @@ fun VaultSpaceCard(
Modifier
}
}
is Wallpaper.Gradient -> {
Modifier.background(
brush = Brush.verticalGradient(
@ -311,6 +347,7 @@ fun VaultSpaceCard(
shape = RoundedCornerShape(20.dp)
)
}
is Wallpaper.Default -> {
Modifier.background(
brush = Brush.verticalGradient(
@ -387,7 +424,7 @@ fun VaultSpaceCard(
}
Spacer(modifier = Modifier.width(8.dp))
}
if (unreadMessageCount > 0) {
val shape = if (unreadMentionCount > 9) {
CircleShape
@ -511,13 +548,48 @@ fun LoadingSpaceCardPreview() {
}
@Composable
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
fun VaultScreenToolbarPreview() {
@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode - Not Scrolled"
)
@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Light Mode - Not Scrolled"
)
fun VaultScreenToolbarNotScrolledPreview() {
VaultScreenToolbar(
onPlusClicked = {},
onSettingsClicked = {},
profile = AccountProfile.Idle
profile = AccountProfile.Data(
name = "John Doe",
icon = ProfileIconView.Placeholder(name = "Jd")
),
isScrolled = false
)
}
@Composable
@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode - Scrolled"
)
@Preview(
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Light Mode - Scrolled"
)
fun VaultScreenToolbarScrolledPreview() {
VaultScreenToolbar(
onPlusClicked = {},
onSettingsClicked = {},
profile = AccountProfile.Data(
name = "John Doe",
icon = ProfileIconView.Placeholder(name = "Jd")
),
isScrolled = true
)
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="55dp"
android:height="56dp"
android:viewportWidth="55"
android:viewportHeight="56">
<path
android:pathData="M24.205,14C37.46,14 48.205,18.477 48.205,24V25.774C49.709,25.844 51.091,26.299 52.236,27.248C54.095,28.789 54.899,31.269 54.899,34.106C54.899,37.278 53.417,40.013 51.314,42.021C49.303,43.944 46.634,45.298 43.775,45.896C39.425,52.011 32.281,56 24.205,56C10.95,56 0.205,45.255 0.205,32V24C0.205,18.477 10.95,14 24.205,14ZM44.205,29.527C39.905,32.222 32.554,34 24.205,34C15.856,34 8.505,32.222 4.205,29.527V32C4.205,43.046 13.159,52 24.205,52C35.251,52 44.205,43.046 44.205,32V29.527ZM48.205,32C48.205,35.066 47.628,37.997 46.58,40.692C47.458,40.215 48.248,39.645 48.916,39.007C50.449,37.542 51.249,35.829 51.249,34.106C51.249,32.05 50.685,30.977 50.034,30.438C49.63,30.103 49.037,29.846 48.205,29.781V32ZM30.205,0C31.31,0 32.205,0.895 32.205,2C32.205,2.151 32.186,2.297 32.154,2.438L32.156,2.439C31.745,4.263 30.246,5.655 28.447,6.543C26.593,7.459 24.137,8 21.205,8C18.737,8 16.917,8.459 15.734,9.043C14.495,9.655 14.196,10.264 14.156,10.439L14.154,10.439C13.954,11.332 13.159,12 12.205,12C11.101,12 10.205,11.105 10.205,10C10.205,9.849 10.223,9.702 10.255,9.561H10.254C10.665,7.736 12.165,6.345 13.963,5.457C15.818,4.541 18.273,4 21.205,4C23.674,4 25.493,3.541 26.676,2.957C27.913,2.346 28.214,1.737 28.254,1.561H28.255C28.455,0.668 29.252,0 30.205,0ZM46.205,0C47.31,0 48.205,0.895 48.205,2C48.205,4.439 46.663,6.985 44.031,8.836C41.351,10.721 37.425,12 32.205,12C31.101,12 30.205,11.105 30.205,10C30.205,8.895 31.101,8 32.205,8C36.786,8 39.861,6.879 41.73,5.564C43.648,4.216 44.205,2.761 44.205,2C44.205,0.895 45.1,0 46.205,0Z"
android:fillColor="@color/shape_primary"/>
</vector>

View file

@ -2077,4 +2077,6 @@ Please provide specific details of your needs here.</string>
<string name="vault_create_space">Space</string>
<string name="vault_create_space_description">For organized content and data</string>
<string name="vault_empty_state_text">There are no spaces yet</string>
</resources>