1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2905 Primitives | Epic | Foundation for primitives (#2098)

Co-authored-by: Evgenii Kozlov <enklave.mare.balticum@protonmail.com>
This commit is contained in:
Konstantin Ivanov 2025-02-28 20:47:43 +01:00 committed by GitHub
parent 88aa30d64b
commit 4bc1e060f3
Signed by: github
GPG key ID: B5690EEEBB952194
153 changed files with 10877 additions and 1616 deletions

View file

@ -1,20 +1,9 @@
package com.anytypeio.anytype.feature_date.mapping
import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.RelationListWithValueItem
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.objects.getProperType
import timber.log.Timber
suspend fun List<RelationListWithValueItem>.toUiFieldsItem(
@ -52,33 +41,4 @@ suspend fun List<RelationListWithValueItem>.toUiFieldsItem(
)
}
}
}
fun ObjectWrapper.Basic.toUiObjectsListItem(
space: SpaceId,
urlBuilder: UrlBuilder,
objectTypes: List<ObjectWrapper.Type>,
fieldParser: FieldParser,
isOwnerOrEditor: Boolean
): UiObjectsListItem {
val obj = this
val typeUrl = obj.getProperType()
val isProfile = typeUrl == MarketplaceObjectTypeIds.PROFILE
val layout = obj.layout ?: ObjectType.Layout.BASIC
return UiObjectsListItem.Item(
id = obj.id,
space = space,
name = fieldParser.getObjectName(obj),
type = typeUrl,
typeName = objectTypes.firstOrNull { type ->
if (isProfile) {
type.uniqueKey == ObjectTypeUniqueKeys.PROFILE
} else {
type.id == typeUrl
}
}?.name,
layout = layout,
icon = obj.objectIcon(builder = urlBuilder),
isPossibleToDelete = isOwnerOrEditor && !restrictions.contains(ObjectRestriction.DELETE)
)
}

View file

@ -24,7 +24,6 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
@ -32,18 +31,19 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.foundation.components.BottomNavigationMenu
import com.anytypeio.anytype.core_ui.lists.objects.PaginatedObjectList
import com.anytypeio.anytype.core_ui.lists.objects.UiContentState
import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState
import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
import com.anytypeio.anytype.feature_date.R
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
import com.anytypeio.anytype.feature_date.viewmodel.UiCalendarIconState
import com.anytypeio.anytype.feature_date.viewmodel.UiCalendarState
import com.anytypeio.anytype.feature_date.viewmodel.UiContentState
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsSheetState
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsState
import com.anytypeio.anytype.feature_date.viewmodel.UiHeaderState
import com.anytypeio.anytype.feature_date.viewmodel.UiNavigationWidget
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListState
import com.anytypeio.anytype.feature_date.viewmodel.UiSnackbarState
import com.anytypeio.anytype.feature_date.viewmodel.UiSyncStatusBadgeState
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
@ -68,8 +68,6 @@ fun DateMainScreen(
onDateEvent: (DateEvent) -> Unit
) {
val scope = rememberCoroutineScope()
val snackBarHostState = remember { SnackbarHostState() }
val snackBarText = stringResource(R.string.all_content_snackbar_title)
@ -156,11 +154,19 @@ fun DateMainScreen(
if (uiContentState is UiContentState.Empty) {
EmptyScreen()
}
ObjectsScreen(
PaginatedObjectList(
state = uiObjectsListState,
uiState = uiContentState,
canPaginate = canPaginate,
onDateEvent = onDateEvent,
onLoadMore = {
DateEvent.ObjectsList.OnLoadMore
},
onMoveToBin = { item ->
DateEvent.ObjectsList.OnObjectMoveToBin(item)
},
onObjectClicked = { item ->
DateEvent.ObjectsList.OnObjectClicked(item)
}
)
BottomNavigationMenu(
modifier = Modifier

View file

@ -1,337 +0,0 @@
package com.anytypeio.anytype.feature_date.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Text
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.ShimmerEffect
import com.anytypeio.anytype.core_ui.extensions.swapList
import com.anytypeio.anytype.core_ui.foundation.DismissBackground
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.animations.DotsLoadingIndicator
import com.anytypeio.anytype.core_ui.views.animations.FadeAnimationSpecs
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.feature_date.R
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
import com.anytypeio.anytype.feature_date.ui.models.StubVerticalItems
import com.anytypeio.anytype.feature_date.viewmodel.UiContentState
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun ObjectsScreen(
state: UiObjectsListState,
uiState: UiContentState,
canPaginate: Boolean,
onDateEvent: (DateEvent) -> Unit
) {
val items = remember { mutableStateListOf<UiObjectsListItem>() }
items.swapList(state.items)
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
val canPaginateState = remember { mutableStateOf(false) }
LaunchedEffect(key1 = canPaginate) {
canPaginateState.value = canPaginate
}
val shouldStartPaging = remember {
derivedStateOf {
canPaginateState.value && (lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
?: -9) >= (lazyListState.layoutInfo.totalItemsCount - 2)
}
}
LaunchedEffect(key1 = shouldStartPaging.value) {
if (shouldStartPaging.value && uiState is UiContentState.Idle) {
onDateEvent(DateEvent.ObjectsList.OnLoadMore)
}
}
LazyColumn(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxSize(),
state = lazyListState
) {
items(
count = items.size,
key = { index -> items[index].id },
contentType = { index ->
when (items[index]) {
is UiObjectsListItem.Loading -> "loading"
is UiObjectsListItem.Item -> "item"
}
}
) { index ->
val item = items[index]
when (item) {
is UiObjectsListItem.Item -> {
SwipeToDismissListItems(
modifier = Modifier
.fillMaxWidth()
.animateItem()
.noRippleThrottledClickable {
onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item))
},
item = item,
onDateEvent = onDateEvent
)
Divider(paddingStart = 16.dp, paddingEnd = 16.dp)
}
is UiObjectsListItem.Loading -> {
ListItemLoading(modifier = Modifier)
}
}
}
if (uiState is UiContentState.Paging) {
item {
Box(
modifier = Modifier
.fillParentMaxWidth()
.height(52.dp),
contentAlignment = Alignment.Center
) {
LoadingState()
}
}
}
item {
Spacer(modifier = Modifier.height(200.dp))
}
}
LaunchedEffect(key1 = uiState) {
if (uiState is UiContentState.Idle) {
if (uiState.scrollToTop) {
scope.launch {
lazyListState.scrollToItem(0)
}
}
}
}
}
@Composable
private fun ListItem(
modifier: Modifier,
item: UiObjectsListItem.Item
) {
val name = item.name.trim().ifBlank { stringResource(R.string.untitled) }
val createdBy = item.createdBy
val typeName = item.typeName
ListItem(
colors = ListItemDefaults.colors(
containerColor = colorResource(id = R.color.background_primary),
),
modifier = modifier
.height(72.dp)
.fillMaxWidth(),
headlineContent = {
Text(
text = name,
style = PreviewTitle2Regular,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
supportingContent = {
Row {
if (typeName != null) {
Text(
text = typeName,
style = Relations3,
color = colorResource(id = R.color.text_secondary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (!createdBy.isNullOrBlank()) {
Text(
text = "${stringResource(R.string.date_layout_item_created_by)}$createdBy",
style = Relations3,
color = colorResource(id = R.color.text_secondary),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
},
leadingContent = {
ListWidgetObjectIcon(icon = item.icon, modifier = Modifier, iconSize = 48.dp)
}
)
}
@Composable
private fun ListItemLoading(
modifier: Modifier
) {
ListItem(
colors = ListItemDefaults.colors(
containerColor = colorResource(id = R.color.background_primary),
),
modifier = modifier
.height(72.dp)
.fillMaxWidth(),
headlineContent = {
ShimmerEffect(
modifier = Modifier
.width(164.dp)
.height(18.dp)
)
},
supportingContent = {
ShimmerEffect(
modifier = Modifier
.width(64.dp)
.height(13.dp)
)
},
leadingContent = {
ShimmerEffect(
modifier = Modifier
.size(48.dp)
)
}
)
}
@Composable
fun SwipeToDismissListItems(
item: UiObjectsListItem.Item,
modifier: Modifier,
animationDuration: Int = 500,
onDateEvent: (DateEvent) -> Unit,
) {
var isRemoved by remember { mutableStateOf(false) }
val dismissState = rememberSwipeToDismissBoxState(
initialValue = SwipeToDismissBoxValue.Settled,
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
isRemoved = true
true
} else {
false
}
return@rememberSwipeToDismissBoxState true
},
positionalThreshold = { it * .5f }
)
if (dismissState.currentValue != SwipeToDismissBoxValue.Settled) {
LaunchedEffect(Unit) {
dismissState.snapTo(SwipeToDismissBoxValue.Settled)
}
}
LaunchedEffect(key1 = isRemoved) {
if (isRemoved) {
delay(animationDuration.toLong())
onDateEvent(DateEvent.ObjectsList.OnObjectMoveToBin(item))
}
}
AnimatedVisibility(
visible = !isRemoved,
exit = shrinkVertically(
animationSpec = tween(durationMillis = animationDuration),
shrinkTowards = Alignment.Top
) + fadeOut()
) {
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
enableDismissFromEndToStart = item.isPossibleToDelete,
enableDismissFromStartToEnd = false,
backgroundContent = {
DismissBackground(
actionText = stringResource(R.string.move_to_bin),
dismissState = dismissState
)
},
content = {
ListItem(
modifier = Modifier
.noRippleThrottledClickable {
onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item))
},
item = item
)
}
)
}
}
@Composable
private fun BoxScope.LoadingState() {
val loadingAlpha by animateFloatAsState(targetValue = 1f, label = "")
DotsLoadingIndicator(
animating = true,
modifier = Modifier
.graphicsLayer { alpha = loadingAlpha }
.align(Alignment.Center),
animationSpecs = FadeAnimationSpecs(itemCount = 3),
color = colorResource(id = R.color.glyph_active),
size = ButtonSize.Small
)
}
@Composable
@DefaultPreviews
fun ObjectsListScreenPreview() {
val contentListState = UiObjectsListState(
items = StubVerticalItems
)
ObjectsScreen(
state = contentListState,
uiState = UiContentState.Idle(scrollToTop = false),
canPaginate = true,
onDateEvent = {}
)
}

View file

@ -4,7 +4,7 @@ import com.anytypeio.anytype.core_models.TimeInMillis
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem
import com.anytypeio.anytype.presentation.objects.UiObjectsListItem
sealed class DateEvent {

View file

@ -1,49 +1,9 @@
package com.anytypeio.anytype.feature_date.ui.models
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.RelationKey
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem
import com.anytypeio.anytype.presentation.objects.ObjectIcon
val StubVerticalItems = listOf(
UiObjectsListItem.Item(
id = "1",
name = "Task Object",
space = SpaceId("space1"),
type = "type1",
typeName = "Task",
createdBy = "by Joseph Wolf",
layout = ObjectType.Layout.TODO,
icon = ObjectIcon.Task(isChecked = true)
),
UiObjectsListItem.Item(
id = "2",
name = "Page Object",
space = SpaceId("space2"),
type = "type2",
typeName = "Page",
createdBy = "by Mike Long",
layout = ObjectType.Layout.BASIC,
icon = ObjectIcon.Empty.Page
),
UiObjectsListItem.Item(
id = "3",
name = "File Object",
space = SpaceId("space3"),
type = "type3",
typeName = "File",
createdBy = "by John Doe",
layout = ObjectType.Layout.FILE,
icon = ObjectIcon.File(
mime = "image/png",
fileName = "test_image.png"
)
)
)
val StubHorizontalItems = listOf(
UiFieldsItem.Settings(),

View file

@ -2,7 +2,6 @@ package com.anytypeio.anytype.feature_date.viewmodel
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.RelativeDate
import com.anytypeio.anytype.core_models.TimeInMillis
@ -11,7 +10,6 @@ import com.anytypeio.anytype.core_models.primitives.RelationKey
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem.Loading
import com.anytypeio.anytype.presentation.objects.ObjectIcon
data class DateObjectVmParams(
val objectId: Id,
@ -100,55 +98,12 @@ sealed class UiFieldsItem {
}
}
data class UiObjectsListState(
val items: List<UiObjectsListItem>
) {
companion object {
val Empty = UiObjectsListState(items = emptyList())
val LoadingState = UiObjectsListState(
items = listOf(
UiObjectsListItem.Loading("Loading-Item-1"),
UiObjectsListItem.Loading("Loading-Item-2"),
UiObjectsListItem.Loading("Loading-Item-3"),
UiObjectsListItem.Loading("Loading-Item-4"),
)
)
}
}
sealed class UiObjectsListItem {
abstract val id: String
data class Loading(override val id: String) : UiObjectsListItem()
data class Item(
override val id: String,
val name: String,
val space: SpaceId,
val type: String? = null,
val typeName: String? = null,
val createdBy: String? = null,
val layout: ObjectType.Layout? = null,
val icon: ObjectIcon = ObjectIcon.None,
val isPossibleToDelete: Boolean = false
) : UiObjectsListItem()
}
sealed class UiNavigationWidget {
data object Hidden : UiNavigationWidget()
data object Editor : UiNavigationWidget()
data object Viewer : UiNavigationWidget()
}
sealed class UiContentState {
data class Idle(val scrollToTop: Boolean = false) : UiContentState()
data object InitLoading : UiContentState()
data object Paging : UiContentState()
data object Empty : UiContentState()
}
sealed class UiFieldsSheetState {
data object Hidden : UiFieldsSheetState()
data class Visible(

View file

@ -14,6 +14,9 @@ import com.anytypeio.anytype.core_models.TimeInSeconds
import com.anytypeio.anytype.core_models.getSingleValue
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds
import com.anytypeio.anytype.core_ui.lists.objects.UiContentState
import com.anytypeio.anytype.core_ui.lists.objects.UiContentState.Idle
import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.library.StoreSearchParams
@ -29,11 +32,9 @@ import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.relations.GetObjectRelationListById
import com.anytypeio.anytype.feature_date.viewmodel.UiErrorState.Reason
import com.anytypeio.anytype.feature_date.mapping.toUiFieldsItem
import com.anytypeio.anytype.feature_date.mapping.toUiObjectsListItem
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
import com.anytypeio.anytype.feature_date.viewmodel.UiContentState.*
import com.anytypeio.anytype.feature_date.viewmodel.UiErrorState.Reason
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.extension.sendAnalyticsClickDateBack
import com.anytypeio.anytype.presentation.extension.sendAnalyticsClickDateCalendarView
@ -44,7 +45,9 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenDate
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSwitchRelationDate
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.home.navigation
import com.anytypeio.anytype.presentation.objects.UiObjectsListItem
import com.anytypeio.anytype.presentation.objects.getCreateObjectParams
import com.anytypeio.anytype.presentation.objects.toUiObjectsListItem
import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel.Companion.DEFAULT_DEBOUNCE_DURATION
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState