From 4582af43bf8b8e639c920d3159785257abd70aa7 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:17:39 +0200 Subject: [PATCH] DROID-3590 Vault 2.0 | Empty state (#2488) --- .../anytype/ui/vault/VaultEmptyScreen.kt | 69 ++++ .../anytypeio/anytype/ui/vault/VaultScreen.kt | 338 +++++++++++------- .../res/drawable/ic_vault_create_space.xml | 9 + localization/src/main/res/values/strings.xml | 2 + 4 files changed, 285 insertions(+), 133 deletions(-) create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt create mode 100644 core-ui/src/main/res/drawable/ic_vault_create_space.xml diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt new file mode 100644 index 0000000000..00898f7f4a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt @@ -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 = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt index 03fe70173a..5e605785fa 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt @@ -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 ) } diff --git a/core-ui/src/main/res/drawable/ic_vault_create_space.xml b/core-ui/src/main/res/drawable/ic_vault_create_space.xml new file mode 100644 index 0000000000..80f4f5316e --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_vault_create_space.xml @@ -0,0 +1,9 @@ + + + diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 961907f56e..3efe1f971e 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -2077,4 +2077,6 @@ Please provide specific details of your needs here. Space For organized content and data + There are no spaces yet + \ No newline at end of file