mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-2793 Date as an Object | Epic (#1782)
Co-authored-by: Evgenii Kozlov <enklave.mare.balticum@protonmail.com> Co-authored-by: Evgenii Kozlov <ubuphobos@gmail.com>
This commit is contained in:
parent
2b40f21910
commit
ca8721b725
284 changed files with 6589 additions and 1211 deletions
60
feature-date/build.gradle
Normal file
60
feature-date/build.gradle
Normal file
|
@ -0,0 +1,60 @@
|
|||
plugins {
|
||||
id "com.android.library"
|
||||
id "kotlin-android"
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
buildConfigField "boolean", "USE_NEW_WINDOW_INSET_API", "true"
|
||||
buildConfigField "boolean", "USE_EDGE_TO_EDGE", "true"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
|
||||
namespace 'com.anytypeio.anytype.feature_date'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation project(':domain')
|
||||
implementation project(':core-ui')
|
||||
implementation project(':analytics')
|
||||
implementation project(':core-models')
|
||||
implementation project(':core-utils')
|
||||
implementation project(':localization')
|
||||
implementation project(':presentation')
|
||||
implementation project(':library-emojifier')
|
||||
|
||||
compileOnly libs.javaxInject
|
||||
|
||||
implementation libs.lifecycleViewModel
|
||||
implementation libs.lifecycleRuntime
|
||||
|
||||
implementation libs.appcompat
|
||||
implementation libs.compose
|
||||
implementation libs.composeFoundation
|
||||
implementation libs.composeToolingPreview
|
||||
implementation libs.composeMaterial3
|
||||
implementation libs.composeMaterial
|
||||
implementation libs.navigationCompose
|
||||
|
||||
debugImplementation libs.composeTooling
|
||||
|
||||
implementation libs.timber
|
||||
|
||||
testImplementation project(':test:android-utils')
|
||||
testImplementation project(':test:utils')
|
||||
testImplementation project(":test:core-models-stub")
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.kotlinTest
|
||||
testImplementation libs.robolectric
|
||||
testImplementation libs.androidXTestCore
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.coroutineTesting
|
||||
testImplementation libs.timberJUnit
|
||||
testImplementation libs.turbine
|
||||
}
|
2
feature-date/src/main/AndroidManifest.xml
Normal file
2
feature-date/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest/>
|
|
@ -0,0 +1,81 @@
|
|||
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.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(
|
||||
storeOfRelations: StoreOfRelations
|
||||
): List<UiFieldsItem.Item> {
|
||||
return this
|
||||
.sortedByDescending { it.key.key == Relations.MENTIONS }
|
||||
.mapNotNull { item ->
|
||||
val relation = storeOfRelations.getByKey(item.key.key)
|
||||
if (relation == null) {
|
||||
Timber.e("Relation ${item.key.key} not found in the relation store")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (relation.key == Relations.LINKS || relation.key == Relations.BACKLINKS) {
|
||||
Timber.w("Relation ${item.key.key} is LINKS or BACKLINKS")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (relation.key != Relations.MENTIONS && relation.isHidden == true) {
|
||||
Timber.w("Relation ${item.key.key} is hidden")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (relation.key == Relations.MENTIONS) {
|
||||
UiFieldsItem.Item.Mention(
|
||||
id = item.key.key,
|
||||
key = item.key,
|
||||
title = relation.name.orEmpty(),
|
||||
relationFormat = relation.format
|
||||
)
|
||||
} else {
|
||||
UiFieldsItem.Item.Default(
|
||||
id = item.key.key,
|
||||
key = item.key,
|
||||
title = relation.name.orEmpty(),
|
||||
relationFormat = relation.format
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ObjectWrapper.Basic.toUiObjectsListItem(
|
||||
space: SpaceId,
|
||||
urlBuilder: UrlBuilder,
|
||||
objectTypes: List<ObjectWrapper.Type>,
|
||||
fieldParser: FieldParser
|
||||
): 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)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.relations.DatePickerContent
|
||||
import com.anytypeio.anytype.feature_date.R
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiCalendarState
|
||||
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
|
||||
import com.anytypeio.anytype.presentation.sets.DateValueView
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CalendarScreen(
|
||||
uiState: UiCalendarState,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
ModalBottomSheet(
|
||||
dragHandle = null,
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = { onDateEvent(DateEvent.Calendar.OnCalendarDismiss) },
|
||||
content = {
|
||||
when (uiState) {
|
||||
is UiCalendarState.Calendar -> {
|
||||
DatePickerContent(
|
||||
state = DateValueView(timeInMillis = uiState.timeInMillis),
|
||||
showHeader = false,
|
||||
onDateSelected = { onDateEvent(DateEvent.Calendar.OnCalendarDateSelected(it)) },
|
||||
onTodayClicked = { onDateEvent(DateEvent.Calendar.OnTodayClick) },
|
||||
onTomorrowClicked = { onDateEvent(DateEvent.Calendar.OnTomorrowClick) }
|
||||
)
|
||||
}
|
||||
|
||||
UiCalendarState.Hidden -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.primitives.RelationKey
|
||||
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.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
|
||||
import com.anytypeio.anytype.feature_date.R
|
||||
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
|
||||
import com.anytypeio.anytype.feature_date.ui.models.StubHorizontalItems
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsState
|
||||
|
||||
@Composable
|
||||
fun FieldsScreen(
|
||||
uiState: UiFieldsState,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
|
||||
val lazyFieldsListState = rememberLazyListState()
|
||||
|
||||
val items = remember { mutableStateListOf<UiFieldsItem>() }
|
||||
items.swapList(uiState.items)
|
||||
|
||||
// Effect to scroll to the selected item when needToScrollTo and selectedRelationKey are set
|
||||
LaunchedEffect(uiState.needToScrollTo, uiState.selectedRelationKey) {
|
||||
if (uiState.needToScrollTo && uiState.selectedRelationKey != null) {
|
||||
val relationKey = uiState.selectedRelationKey
|
||||
val index = items.indexOfFirst { it.id == relationKey.key }
|
||||
if (index != -1) {
|
||||
lazyFieldsListState.animateScrollToItem(index)
|
||||
onDateEvent(DateEvent.FieldsList.OnScrolledToItemDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
state = lazyFieldsListState,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 24.dp)
|
||||
) {
|
||||
items(
|
||||
count = items.size,
|
||||
key = { items[it].id },
|
||||
contentType = { index ->
|
||||
when (items[index]) {
|
||||
is UiFieldsItem.Settings -> "settings"
|
||||
is UiFieldsItem.Item -> "item"
|
||||
is UiFieldsItem.Loading -> "loading"
|
||||
}
|
||||
}
|
||||
) {
|
||||
val item = items[it]
|
||||
val background = if (uiState.selectedRelationKey?.key == item.id) {
|
||||
colorResource(R.color.shape_secondary)
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
.wrapContentWidth()
|
||||
.background(
|
||||
color = background,
|
||||
shape = RoundedCornerShape(size = 10.dp)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(R.color.shape_primary),
|
||||
shape = RoundedCornerShape(size = 10.dp)
|
||||
)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(DateEvent.FieldsList.OnFieldClick(item))
|
||||
}
|
||||
) {
|
||||
when (item) {
|
||||
is UiFieldsItem.Settings -> {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(24.dp)
|
||||
.align(Alignment.Center),
|
||||
painter = painterResource(R.drawable.ic_burger_24),
|
||||
contentDescription = "List of date relations"
|
||||
)
|
||||
}
|
||||
|
||||
is UiFieldsItem.Item.Mention -> {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillParentMaxHeight()
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_mention_24),
|
||||
contentDescription = "Mentioned in"
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(),
|
||||
text = stringResource(R.string.date_layout_mentioned_in),
|
||||
color = colorResource(R.color.text_primary),
|
||||
style = PreviewTitle2Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is UiFieldsItem.Item.Default -> {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.wrapContentSize()
|
||||
.align(Alignment.Center),
|
||||
text = item.title,
|
||||
color = colorResource(R.color.text_primary),
|
||||
style = PreviewTitle2Medium
|
||||
)
|
||||
}
|
||||
|
||||
is UiFieldsItem.Loading.Item -> {
|
||||
ShimmerEffect(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Center)
|
||||
.width(88.dp)
|
||||
.height(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
is UiFieldsItem.Loading.Settings -> {
|
||||
ShimmerEffect(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Center)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun FieldsScreenPreview() {
|
||||
FieldsScreen(
|
||||
uiState = UiFieldsState(
|
||||
items = StubHorizontalItems,
|
||||
selectedRelationKey = RelationKey("1")
|
||||
),
|
||||
onDateEvent = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun LoadingPreview() {
|
||||
FieldsScreen(
|
||||
uiState = UiFieldsState.LoadingState,
|
||||
onDateEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
import com.anytypeio.anytype.core_ui.foundation.Dragger
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.feature_date.R
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsSheetState
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem
|
||||
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
|
||||
import com.anytypeio.anytype.feature_date.ui.models.StubHorizontalItems
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FieldsSheetScreen(
|
||||
uiState: UiFieldsSheetState,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
ModalBottomSheet(
|
||||
dragHandle = {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Dragger()
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
},
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
onDateEvent(DateEvent.FieldsSheet.OnSheetDismiss)
|
||||
},
|
||||
content = {
|
||||
when (uiState) {
|
||||
is UiFieldsSheetState.Visible -> {
|
||||
DateObjectSheetScreen(
|
||||
uiSheetState = uiState,
|
||||
onDateEvent = onDateEvent
|
||||
)
|
||||
}
|
||||
|
||||
UiFieldsSheetState.Hidden -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DateObjectSheetScreen(
|
||||
uiSheetState: UiFieldsSheetState.Visible,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
Spacer(Modifier.height(10.dp))
|
||||
SearchBar(onDateEvent = onDateEvent)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Divider(paddingStart = 0.dp, paddingEnd = 0.dp)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState
|
||||
) {
|
||||
items(
|
||||
count = uiSheetState.items.size,
|
||||
key = { index -> uiSheetState.items[index].id },
|
||||
itemContent = { index ->
|
||||
when (val item = uiSheetState.items[index]) {
|
||||
is UiFieldsItem.Item.Default -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
.height(52.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(DateEvent.FieldsSheet.OnFieldClick(item))
|
||||
},
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = item.title,
|
||||
maxLines = 1,
|
||||
color = colorResource(R.color.text_primary),
|
||||
style = BodyRegular,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
is UiFieldsItem.Item.Mention -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
.height(52.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(DateEvent.FieldsSheet.OnFieldClick(item))
|
||||
},
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillParentMaxHeight()
|
||||
.wrapContentWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_mention_24),
|
||||
contentDescription = "Mentioned in"
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(),
|
||||
text = stringResource(R.string.date_layout_mentioned_in),
|
||||
color = colorResource(R.color.text_primary),
|
||||
style = BodyRegular
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
else -> {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(64.dp))
|
||||
}
|
||||
|
||||
//region SearchBar
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SearchBar(onDateEvent: (DateEvent) -> Unit) {
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focus = LocalFocusManager.current
|
||||
val focusRequester = FocusRequester()
|
||||
|
||||
val selectionColors = TextSelectionColors(
|
||||
backgroundColor = colorResource(id = R.color.cursor_color).copy(
|
||||
alpha = 0.2f
|
||||
),
|
||||
handleColor = colorResource(id = R.color.cursor_color),
|
||||
)
|
||||
|
||||
var query by remember { mutableStateOf(TextFieldValue()) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_transparent),
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
)
|
||||
.height(40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_search_18),
|
||||
contentDescription = "Search icon",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(
|
||||
start = 11.dp
|
||||
)
|
||||
)
|
||||
CompositionLocalProvider(value = LocalTextSelectionColors provides selectionColors) {
|
||||
|
||||
BasicTextField(
|
||||
value = query,
|
||||
modifier = Modifier
|
||||
.weight(1.0f)
|
||||
.padding(start = 6.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.focusRequester(focusRequester),
|
||||
textStyle = BodyRegular.copy(
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
),
|
||||
onValueChange = { input ->
|
||||
query = input.also {
|
||||
onDateEvent(DateEvent.FieldsSheet.OnSearchQueryChanged(input.text))
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focus.clearFocus(true)
|
||||
}
|
||||
),
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
||||
value = query.text,
|
||||
innerTextField = innerTextField,
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.search),
|
||||
style = BodyRegular.copy(
|
||||
color = colorResource(id = R.color.text_tertiary)
|
||||
)
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Transparent,
|
||||
cursorColor = colorResource(id = R.color.cursor_color),
|
||||
),
|
||||
border = {},
|
||||
contentPadding = PaddingValues()
|
||||
)
|
||||
},
|
||||
cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue)),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(9.dp))
|
||||
AnimatedVisibility(
|
||||
visible = query.text.isNotEmpty(),
|
||||
enter = fadeIn(tween(100)),
|
||||
exit = fadeOut(tween(100))
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_clear_18),
|
||||
contentDescription = "Clear icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 9.dp)
|
||||
.noRippleClickable {
|
||||
query = TextFieldValue().also {
|
||||
onDateEvent(DateEvent.FieldsSheet.OnSearchQueryChanged(""))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
private fun SearchBarPreview() {
|
||||
Column {
|
||||
DateObjectSheetScreen(
|
||||
uiSheetState = UiFieldsSheetState.Visible(
|
||||
items = StubHorizontalItems
|
||||
),
|
||||
onDateEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
|
@ -0,0 +1,143 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
|
||||
import com.anytypeio.anytype.feature_date.R
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiHeaderState
|
||||
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
|
||||
|
||||
@Composable
|
||||
fun HeaderScreen(
|
||||
modifier: Modifier,
|
||||
uiState: UiHeaderState,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(52.dp)
|
||||
.rotate(180f)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(DateEvent.Header.OnPreviousClick)
|
||||
},
|
||||
contentDescription = "Previous day",
|
||||
painter = painterResource(id = R.drawable.ic_arrow_disclosure_18),
|
||||
contentScale = ContentScale.None
|
||||
)
|
||||
when (uiState) {
|
||||
is UiHeaderState.Content -> {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = uiState.title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
style = HeadlineTitle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
}
|
||||
|
||||
UiHeaderState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ShimmerEffect(
|
||||
modifier = Modifier
|
||||
.width(200.dp)
|
||||
.height(30.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
UiHeaderState.Empty -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(200.dp)
|
||||
.height(30.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(52.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(DateEvent.Header.OnNextClick)
|
||||
},
|
||||
contentDescription = "Next day",
|
||||
painter = painterResource(id = R.drawable.ic_arrow_disclosure_18),
|
||||
contentScale = ContentScale.None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun DateLayoutHeaderEmptyPreview() {
|
||||
val state = UiHeaderState.Empty
|
||||
HeaderScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp), uiState = state
|
||||
) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun DateLayoutHeaderLoadingPreview() {
|
||||
val state = UiHeaderState.Loading
|
||||
HeaderScreen(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp), state
|
||||
) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun DateLayoutHeaderPreview() {
|
||||
val state = UiHeaderState.Content("Tue, 12 Oct")
|
||||
HeaderScreen(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp), state
|
||||
) {}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.foundation.components.BottomNavigationMenu
|
||||
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.UiSyncStatusBadgeState
|
||||
import com.anytypeio.anytype.feature_date.viewmodel.UiSyncStatusWidgetState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DateMainScreen(
|
||||
uiCalendarIconState: UiCalendarIconState,
|
||||
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
|
||||
uiHeaderState: UiHeaderState,
|
||||
uiFieldsState: UiFieldsState,
|
||||
uiObjectsListState: UiObjectsListState,
|
||||
uiNavigationWidget: UiNavigationWidget,
|
||||
uiFieldsSheetState: UiFieldsSheetState,
|
||||
uiSyncStatusState: UiSyncStatusWidgetState,
|
||||
uiCalendarState: UiCalendarState,
|
||||
uiContentState: UiContentState,
|
||||
canPaginate: Boolean,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
contentColor = colorResource(id = R.color.background_primary),
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = colorResource(id = R.color.background_primary))
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
|
||||
Spacer(
|
||||
modifier = Modifier.windowInsetsTopHeight(
|
||||
WindowInsets.statusBars
|
||||
)
|
||||
)
|
||||
}
|
||||
TopToolbarScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
uiCalendarIconState = uiCalendarIconState,
|
||||
uiSyncStatusBadgeState = uiSyncStatusBadgeState,
|
||||
onDateEvent = onDateEvent
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.height(24.dp)
|
||||
)
|
||||
HeaderScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
uiState = uiHeaderState,
|
||||
onDateEvent = onDateEvent
|
||||
)
|
||||
FieldsScreen(
|
||||
uiState = uiFieldsState,
|
||||
onDateEvent = onDateEvent
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.height(8.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
val contentModifier =
|
||||
if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK)
|
||||
Modifier
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.fillMaxSize()
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
else
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
Box(
|
||||
modifier = contentModifier,
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
if (uiContentState is UiContentState.Empty) {
|
||||
EmptyScreen()
|
||||
}
|
||||
ObjectsScreen(
|
||||
state = uiObjectsListState,
|
||||
uiState = uiContentState,
|
||||
canPaginate = canPaginate,
|
||||
onDateEvent = onDateEvent,
|
||||
)
|
||||
BottomNavigationMenu(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp),
|
||||
backClick = {
|
||||
onDateEvent(DateEvent.NavigationWidget.OnBackClick)
|
||||
},
|
||||
backLongClick = {
|
||||
onDateEvent(DateEvent.NavigationWidget.OnBackLongClick)
|
||||
},
|
||||
searchClick = {
|
||||
onDateEvent(DateEvent.NavigationWidget.OnGlobalSearchClick)
|
||||
},
|
||||
addDocClick = {
|
||||
onDateEvent(DateEvent.NavigationWidget.OnAddDocClick)
|
||||
},
|
||||
addDocLongClick = {
|
||||
onDateEvent(DateEvent.NavigationWidget.OnAddDocLongClick)
|
||||
},
|
||||
isOwnerOrEditor = uiNavigationWidget is UiNavigationWidget.Editor
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (uiSyncStatusState is UiSyncStatusWidgetState.Visible) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
SpaceSyncStatusScreen(
|
||||
uiState = uiSyncStatusState.status,
|
||||
onDismiss = { onDateEvent(DateEvent.SyncStatusWidget.OnSyncStatusDismiss) },
|
||||
scope = scope,
|
||||
onUpdateAppClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiFieldsSheetState is UiFieldsSheetState.Visible) {
|
||||
FieldsSheetScreen(
|
||||
uiState = uiFieldsSheetState,
|
||||
onDateEvent = onDateEvent
|
||||
)
|
||||
}
|
||||
if (uiCalendarState is UiCalendarState.Calendar) {
|
||||
CalendarScreen(
|
||||
uiState = uiCalendarState,
|
||||
onDateEvent = onDateEvent
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
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.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.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.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.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 -> {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item))
|
||||
},
|
||||
item = item
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
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 = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.views.UXBody
|
||||
import com.anytypeio.anytype.feature_date.R
|
||||
|
||||
@Composable
|
||||
fun EmptyScreen() {
|
||||
val title = stringResource(R.string.date_layout_empty_items)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.ime)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
text = title,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
style = UXBody,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package com.anytypeio.anytype.feature_date.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate
|
||||
import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.syncstatus.StatusBadge
|
||||
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.UiSyncStatusBadgeState
|
||||
|
||||
@Composable
|
||||
fun TopToolbarScreen(
|
||||
modifier: Modifier,
|
||||
uiCalendarIconState: UiCalendarIconState,
|
||||
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
|
||||
onDateEvent: (DateEvent) -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
) {
|
||||
if (uiSyncStatusBadgeState is UiSyncStatusBadgeState.Visible) {
|
||||
val s = uiSyncStatusBadgeState.status
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(
|
||||
DateEvent.TopToolbar.OnSyncStatusClick(
|
||||
status = uiSyncStatusBadgeState.status
|
||||
)
|
||||
)
|
||||
},
|
||||
) {
|
||||
StatusBadge(
|
||||
status = uiSyncStatusBadgeState.status,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiCalendarIconState is UiCalendarIconState.Visible) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.noRippleThrottledClickable {
|
||||
onDateEvent(
|
||||
DateEvent.TopToolbar.OnCalendarClick(
|
||||
timestampInSeconds = uiCalendarIconState.timestampInSeconds
|
||||
)
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
painter = painterResource(id = R.drawable.ic_calendar_24),
|
||||
contentScale = ContentScale.None
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun TopToolbarPreview() {
|
||||
val spaceSyncUpdate = SpaceSyncUpdate.Update(
|
||||
id = "1",
|
||||
status = SpaceSyncStatus.SYNCING,
|
||||
network = SpaceSyncNetwork.ANYTYPE,
|
||||
error = SpaceSyncError.NULL,
|
||||
syncingObjectsCounter = 2
|
||||
)
|
||||
TopToolbarScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiCalendarIconState = UiCalendarIconState.Visible(
|
||||
timestampInSeconds = TimestampInSeconds(3232L)
|
||||
),
|
||||
uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible(
|
||||
status = SpaceSyncAndP2PStatusState.Success(
|
||||
spaceSyncUpdate = spaceSyncUpdate,
|
||||
p2PStatusUpdate = P2PStatusUpdate.Initial
|
||||
)
|
||||
),
|
||||
onDateEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package com.anytypeio.anytype.feature_date.ui.models
|
||||
|
||||
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
|
||||
|
||||
sealed class DateEvent {
|
||||
|
||||
sealed class TopToolbar : DateEvent() {
|
||||
data class OnSyncStatusClick(val status: SpaceSyncAndP2PStatusState) : TopToolbar()
|
||||
data class OnCalendarClick(val timestampInSeconds: TimestampInSeconds) : TopToolbar()
|
||||
}
|
||||
|
||||
sealed class Header : DateEvent() {
|
||||
data object OnNextClick : Header()
|
||||
data object OnPreviousClick : Header()
|
||||
}
|
||||
|
||||
sealed class FieldsSheet : DateEvent() {
|
||||
data object OnSheetDismiss : FieldsSheet()
|
||||
data class OnFieldClick(val item: UiFieldsItem) : FieldsSheet()
|
||||
data class OnSearchQueryChanged(val query: String) : FieldsSheet()
|
||||
}
|
||||
|
||||
sealed class Calendar : DateEvent() {
|
||||
data object OnCalendarDismiss : Calendar()
|
||||
data class OnCalendarDateSelected(val timeInMillis: Long?) : Calendar()
|
||||
data object OnTodayClick : Calendar()
|
||||
data object OnTomorrowClick : Calendar()
|
||||
}
|
||||
|
||||
sealed class NavigationWidget : DateEvent() {
|
||||
data object OnGlobalSearchClick : NavigationWidget()
|
||||
data object OnAddDocClick : NavigationWidget()
|
||||
data object OnAddDocLongClick : NavigationWidget()
|
||||
data object OnBackClick : NavigationWidget()
|
||||
data object OnBackLongClick : NavigationWidget()
|
||||
}
|
||||
|
||||
sealed class ObjectsList : DateEvent() {
|
||||
data class OnObjectClicked(val item: UiObjectsListItem) : ObjectsList()
|
||||
data object OnLoadMore : ObjectsList()
|
||||
}
|
||||
|
||||
sealed class FieldsList : DateEvent() {
|
||||
data class OnFieldClick(val item: UiFieldsItem) : FieldsList()
|
||||
data object OnScrolledToItemDismiss : FieldsList()
|
||||
}
|
||||
|
||||
sealed class SyncStatusWidget : DateEvent() {
|
||||
data object OnSyncStatusDismiss : SyncStatusWidget()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
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(),
|
||||
UiFieldsItem.Item.Mention(
|
||||
id = "Item 54",
|
||||
title = "Mentionssssss",
|
||||
key = RelationKey(key = Relations.MENTIONS),
|
||||
relationFormat = RelationFormat.DATE
|
||||
),
|
||||
UiFieldsItem.Item.Default(
|
||||
"Item 1",
|
||||
title = "Title1",
|
||||
key = RelationKey("key1"),
|
||||
relationFormat = RelationFormat.DATE
|
||||
),
|
||||
UiFieldsItem.Item.Default(
|
||||
"Item 2",
|
||||
title = "Title2",
|
||||
key = RelationKey("key2"),
|
||||
relationFormat = RelationFormat.DATE
|
||||
),
|
||||
UiFieldsItem.Item.Default(
|
||||
"Item 3",
|
||||
title = "Title3",
|
||||
key = RelationKey("key3"),
|
||||
relationFormat = RelationFormat.DATE
|
||||
),
|
||||
UiFieldsItem.Item.Default(
|
||||
"Item 4",
|
||||
title = "Title4",
|
||||
key = RelationKey("key4"),
|
||||
relationFormat = RelationFormat.DATE
|
||||
),
|
||||
UiFieldsItem.Item.Default(
|
||||
"Item 5",
|
||||
title = "Title5",
|
||||
key = RelationKey("key5"),
|
||||
relationFormat = RelationFormat.DATE
|
||||
),
|
||||
)
|
|
@ -0,0 +1,179 @@
|
|||
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.TimeInMillis
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
|
||||
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
|
||||
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
|
||||
|
||||
data class DateObjectVmParams(
|
||||
val objectId: Id,
|
||||
val spaceId: SpaceId
|
||||
)
|
||||
|
||||
data class ActiveField(
|
||||
val key: RelationKey,
|
||||
val format: RelationFormat,
|
||||
val sort: DVSortType = DVSortType.DESC
|
||||
)
|
||||
|
||||
sealed class UiHeaderState {
|
||||
|
||||
data object Empty : UiHeaderState()
|
||||
data object Loading : UiHeaderState()
|
||||
data class Content(
|
||||
val title: String
|
||||
) : UiHeaderState()
|
||||
}
|
||||
|
||||
sealed class UiCalendarIconState {
|
||||
data object Hidden : UiCalendarIconState()
|
||||
data class Visible(val timestampInSeconds: TimestampInSeconds) : UiCalendarIconState()
|
||||
}
|
||||
|
||||
sealed class UiSyncStatusBadgeState {
|
||||
data object Hidden : UiSyncStatusBadgeState()
|
||||
data class Visible(val status: SpaceSyncAndP2PStatusState) : UiSyncStatusBadgeState()
|
||||
}
|
||||
|
||||
sealed class UiSyncStatusWidgetState {
|
||||
data object Hidden : UiSyncStatusWidgetState()
|
||||
data class Visible(val status: SyncStatusWidgetState) : UiSyncStatusWidgetState()
|
||||
}
|
||||
|
||||
data class UiFieldsState(
|
||||
val items: List<UiFieldsItem>,
|
||||
val selectedRelationKey: RelationKey? = null,
|
||||
val needToScrollTo: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
|
||||
val Empty = UiFieldsState(items = emptyList())
|
||||
|
||||
val LoadingState =
|
||||
UiFieldsState(
|
||||
items = listOf(
|
||||
UiFieldsItem.Loading.Settings("Loading-Settings"),
|
||||
UiFieldsItem.Loading.Item("Loading-Item-1"),
|
||||
UiFieldsItem.Loading.Item("Loading-Item-2"),
|
||||
UiFieldsItem.Loading.Item("Loading-Item-3"),
|
||||
UiFieldsItem.Loading.Item("Loading-Item-4")
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiFieldsItem {
|
||||
|
||||
abstract val id: String
|
||||
|
||||
sealed class Loading(override val id: String) : UiFieldsItem() {
|
||||
data class Item(override val id: String) : Loading(id)
|
||||
data class Settings(override val id: String) : Loading(id)
|
||||
}
|
||||
|
||||
data class Settings(
|
||||
override val id: String = "UiHorizontalListItem-Settings-Id"
|
||||
) : UiFieldsItem()
|
||||
|
||||
sealed class Item : UiFieldsItem() {
|
||||
abstract val key: RelationKey
|
||||
abstract val relationFormat: RelationFormat
|
||||
abstract val title: String
|
||||
|
||||
data class Default(
|
||||
override val id: String,
|
||||
override val key: RelationKey,
|
||||
override val relationFormat: RelationFormat,
|
||||
override val title: String
|
||||
) : Item()
|
||||
|
||||
data class Mention(
|
||||
override val id: String,
|
||||
override val key: RelationKey,
|
||||
override val relationFormat: RelationFormat,
|
||||
override val title: String
|
||||
) : Item()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
) : 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(
|
||||
val items: List<UiFieldsItem>
|
||||
) : UiFieldsSheetState()
|
||||
}
|
||||
|
||||
sealed class UiCalendarState {
|
||||
data object Hidden : UiCalendarState()
|
||||
data class Calendar(
|
||||
val timeInMillis: TimeInMillis?
|
||||
) : UiCalendarState()
|
||||
}
|
||||
|
||||
sealed class UiErrorState {
|
||||
data object Hidden : UiErrorState()
|
||||
data class Show(val reason: Reason) : UiErrorState()
|
||||
|
||||
sealed class Reason {
|
||||
data class YearOutOfRange(val min: Int, val max: Int) : Reason()
|
||||
data class ErrorGettingFields(val msg: String) : Reason()
|
||||
data class ErrorGettingObjects(val msg: String) : Reason()
|
||||
data class Other(val msg: String) : Reason()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.anytypeio.anytype.feature_date.viewmodel
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
|
||||
sealed class DateObjectCommand {
|
||||
data class OpenChat(val target: Id, val space: SpaceId) : DateObjectCommand()
|
||||
data class NavigateToEditor(val id: Id, val space: SpaceId) : DateObjectCommand()
|
||||
data class NavigateToSetOrCollection(val id: Id, val space: SpaceId) : DateObjectCommand()
|
||||
data class NavigateToDateObject(val objectId: Id, val space: SpaceId) : DateObjectCommand()
|
||||
data object TypeSelectionScreen : DateObjectCommand()
|
||||
data object ExitToSpaceWidgets : DateObjectCommand()
|
||||
sealed class SendToast : DateObjectCommand() {
|
||||
data class UnexpectedLayout(val layout: String) : SendToast()
|
||||
}
|
||||
data object OpenGlobalSearch : DateObjectCommand()
|
||||
data object ExitToVault : DateObjectCommand()
|
||||
data object Back : DateObjectCommand()
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.anytypeio.anytype.feature_date.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
|
||||
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.misc.DateProvider
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.`object`.GetObject
|
||||
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
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.presentation.analytics.AnalyticSpaceHelperDelegate
|
||||
import javax.inject.Inject
|
||||
|
||||
class DateObjectVMFactory @Inject constructor(
|
||||
private val vmParams: DateObjectVmParams,
|
||||
private val getObject: GetObject,
|
||||
private val analytics: Analytics,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
|
||||
private val userPermissionProvider: UserPermissionProvider,
|
||||
private val getObjectRelationListById: GetObjectRelationListById,
|
||||
private val storeOfRelations: StoreOfRelations,
|
||||
private val storeOfObjectTypes: StoreOfObjectTypes,
|
||||
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
|
||||
private val getDateObjectByTimestamp: GetDateObjectByTimestamp,
|
||||
private val dateProvider: DateProvider,
|
||||
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
|
||||
private val createObject: CreateObject,
|
||||
private val fieldParser: FieldParser
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
DateObjectViewModel(
|
||||
vmParams = vmParams,
|
||||
getObject = getObject,
|
||||
analytics = analytics,
|
||||
urlBuilder = urlBuilder,
|
||||
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
|
||||
userPermissionProvider = userPermissionProvider,
|
||||
getObjectRelationListById = getObjectRelationListById,
|
||||
storeOfRelations = storeOfRelations,
|
||||
storeOfObjectTypes = storeOfObjectTypes,
|
||||
storelessSubscriptionContainer = storelessSubscriptionContainer,
|
||||
getDateObjectByTimestamp = getDateObjectByTimestamp,
|
||||
dateProvider = dateProvider,
|
||||
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
|
||||
createObject = createObject,
|
||||
fieldParser = fieldParser
|
||||
) as T
|
||||
}
|
|
@ -0,0 +1,863 @@
|
|||
package com.anytypeio.anytype.feature_date.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.analytics.base.EventsDictionary
|
||||
import com.anytypeio.anytype.core_models.DATE_PICKER_YEAR_RANGE
|
||||
import com.anytypeio.anytype.core_models.DVSortType
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.Struct
|
||||
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.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
|
||||
import com.anytypeio.anytype.domain.library.StoreSearchParams
|
||||
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.misc.DateProvider
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.`object`.GetObject
|
||||
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
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.presentation.analytics.AnalyticSpaceHelperDelegate
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsAllContentScreen
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent
|
||||
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
|
||||
import com.anytypeio.anytype.presentation.home.navigation
|
||||
import com.anytypeio.anytype.presentation.objects.getCreateObjectParams
|
||||
import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel.Companion.DEFAULT_DEBOUNCE_DURATION
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
|
||||
import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState
|
||||
import kotlin.collections.map
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* ViewState: @see [UiContentState]
|
||||
* Factory: @see [DateObjectVMFactory]
|
||||
* Screen: @see [com.anytypeio.anytype.feature_date.ui.DateMainScreen]
|
||||
* Models: @see [UiObjectsListState]
|
||||
*/
|
||||
class DateObjectViewModel(
|
||||
private val vmParams: DateObjectVmParams,
|
||||
private val getObject: GetObject,
|
||||
private val analytics: Analytics,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
|
||||
private val userPermissionProvider: UserPermissionProvider,
|
||||
private val getObjectRelationListById: GetObjectRelationListById,
|
||||
private val storeOfRelations: StoreOfRelations,
|
||||
private val storeOfObjectTypes: StoreOfObjectTypes,
|
||||
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
|
||||
private val getDateObjectByTimestamp: GetDateObjectByTimestamp,
|
||||
private val dateProvider: DateProvider,
|
||||
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
|
||||
private val createObject: CreateObject,
|
||||
private val fieldParser: FieldParser
|
||||
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
|
||||
|
||||
val uiCalendarIconState = MutableStateFlow<UiCalendarIconState>(UiCalendarIconState.Hidden)
|
||||
val uiSyncStatusBadgeState =
|
||||
MutableStateFlow<UiSyncStatusBadgeState>(UiSyncStatusBadgeState.Hidden)
|
||||
val uiHeaderState = MutableStateFlow<UiHeaderState>(UiHeaderState.Empty)
|
||||
val uiNavigationWidget = MutableStateFlow<UiNavigationWidget>(UiNavigationWidget.Hidden)
|
||||
val uiFieldsState = MutableStateFlow<UiFieldsState>(UiFieldsState.Empty)
|
||||
val uiFieldsSheetState = MutableStateFlow<UiFieldsSheetState>(UiFieldsSheetState.Hidden)
|
||||
val uiObjectsListState = MutableStateFlow<UiObjectsListState>(UiObjectsListState.Empty)
|
||||
val uiContentState = MutableStateFlow<UiContentState>(UiContentState.Idle())
|
||||
val uiCalendarState = MutableStateFlow<UiCalendarState>(UiCalendarState.Hidden)
|
||||
val uiSyncStatusWidgetState =
|
||||
MutableStateFlow<UiSyncStatusWidgetState>(UiSyncStatusWidgetState.Hidden)
|
||||
|
||||
val effects = MutableSharedFlow<DateObjectCommand>()
|
||||
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
|
||||
|
||||
private val _dateId = MutableStateFlow<Id?>(null)
|
||||
private val _dateTimestamp = MutableStateFlow<TimeInSeconds?>(null)
|
||||
private val _activeField = MutableStateFlow<ActiveField?>(null)
|
||||
|
||||
/**
|
||||
* Paging and subscription limit. If true, we can paginate after reaching bottom items.
|
||||
* Could be true only after the first subscription results (if results size == limit)
|
||||
*/
|
||||
val canPaginate = MutableStateFlow(false)
|
||||
private var _itemsLimit = DEFAULT_SEARCH_LIMIT
|
||||
private val restartSubscription = MutableStateFlow(0L)
|
||||
|
||||
/**
|
||||
* Search query
|
||||
*/
|
||||
private val userInput = MutableStateFlow("")
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private val searchQuery = userInput
|
||||
.take(1)
|
||||
.onCompletion {
|
||||
emitAll(userInput.drop(1).debounce(DEFAULT_DEBOUNCE_DURATION).distinctUntilChanged())
|
||||
}
|
||||
|
||||
private var shouldScrollToTopItems = false
|
||||
|
||||
private val permission = MutableStateFlow(userPermissionProvider.get(vmParams.spaceId))
|
||||
|
||||
init {
|
||||
Timber.d("Init DateObjectViewModel, date object id: [${vmParams.objectId}], space: [${vmParams.spaceId}]")
|
||||
uiHeaderState.value = UiHeaderState.Loading
|
||||
uiFieldsState.value = UiFieldsState.LoadingState
|
||||
uiObjectsListState.value = UiObjectsListState.LoadingState
|
||||
proceedWithObservingPermissions()
|
||||
proceedWithGettingDateObject()
|
||||
proceedWithGettingDateObjectRelationList()
|
||||
proceedWithObservingSyncStatus()
|
||||
setupSearchStateFlow()
|
||||
_dateId.value = vmParams.objectId
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
Timber.d("onStart")
|
||||
setupUiStateFlow()
|
||||
viewModelScope.launch {
|
||||
sendAnalyticsAllContentScreen(
|
||||
analytics = analytics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
unsubscribe()
|
||||
resetLimit()
|
||||
canPaginate.value = false
|
||||
uiObjectsListState.value = UiObjectsListState.Empty
|
||||
uiContentState.value = UiContentState.Empty
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
Timber.d("onCleared")
|
||||
super.onCleared()
|
||||
uiContentState.value = UiContentState.Empty
|
||||
uiHeaderState.value = UiHeaderState.Empty
|
||||
uiCalendarIconState.value = UiCalendarIconState.Hidden
|
||||
uiSyncStatusBadgeState.value = UiSyncStatusBadgeState.Hidden
|
||||
uiFieldsState.value = UiFieldsState.Empty
|
||||
uiObjectsListState.value = UiObjectsListState.Empty
|
||||
uiFieldsSheetState.value = UiFieldsSheetState.Hidden
|
||||
resetLimit()
|
||||
}
|
||||
|
||||
private fun proceedWithReopenDateObjectByTimestamp(timestamp: TimeInSeconds) {
|
||||
proceedWithGettingDateByTimestamp(
|
||||
timestamp = timestamp
|
||||
) { dateObject ->
|
||||
val id = dateObject?.getSingleValue<String>(Relations.ID)
|
||||
if (id != null) {
|
||||
reopenDateObject(id)
|
||||
} else {
|
||||
Timber.e("GettingDateByTimestamp error, object has no id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reopenDateObject(dateObjectId: Id) {
|
||||
Timber.d("Reopen date object: $dateObjectId")
|
||||
canPaginate.value = false
|
||||
resetLimit()
|
||||
shouldScrollToTopItems = true
|
||||
uiFieldsState.value = UiFieldsState.Empty
|
||||
uiObjectsListState.value = UiObjectsListState.Empty
|
||||
uiFieldsSheetState.value = UiFieldsSheetState.Hidden
|
||||
_activeField.value = null
|
||||
_dateId.value = dateObjectId
|
||||
}
|
||||
|
||||
private fun setupSearchStateFlow() {
|
||||
viewModelScope.launch {
|
||||
searchQuery.collectLatest { query ->
|
||||
if (uiFieldsSheetState.value is UiFieldsSheetState.Hidden) return@collectLatest
|
||||
val items = uiFieldsState.value.items
|
||||
if (items.isEmpty()) return@collectLatest
|
||||
val filteredItems = if (query.isBlank()) {
|
||||
items
|
||||
} else {
|
||||
items.filterIsInstance<UiFieldsItem.Item>()
|
||||
.filter { it.title.contains(query, ignoreCase = true) }
|
||||
}
|
||||
uiFieldsSheetState.value = UiFieldsSheetState.Visible(
|
||||
items = filteredItems
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Initialization
|
||||
private fun proceedWithObservingPermissions() {
|
||||
viewModelScope.launch {
|
||||
userPermissionProvider
|
||||
.observe(space = vmParams.spaceId)
|
||||
.collect { result ->
|
||||
uiNavigationWidget.value = if (result?.isOwnerOrEditor() == true) {
|
||||
UiNavigationWidget.Editor
|
||||
} else {
|
||||
UiNavigationWidget.Viewer
|
||||
}
|
||||
permission.value = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithObservingSyncStatus() {
|
||||
viewModelScope.launch {
|
||||
spaceSyncAndP2PStatusProvider
|
||||
.observe()
|
||||
.catch {
|
||||
Timber.e(it, "Error while observing sync status")
|
||||
}
|
||||
.collect { syncAndP2pState ->
|
||||
Timber.d("Sync status: $syncAndP2pState")
|
||||
uiSyncStatusBadgeState.value = UiSyncStatusBadgeState.Visible(syncAndP2pState)
|
||||
val state = uiSyncStatusWidgetState.value
|
||||
uiSyncStatusWidgetState.value = when (state) {
|
||||
UiSyncStatusWidgetState.Hidden -> UiSyncStatusWidgetState.Hidden
|
||||
is UiSyncStatusWidgetState.Visible -> state.copy(
|
||||
status = syncAndP2pState.toSyncStatusWidgetState()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithGettingDateObjectRelationList() {
|
||||
viewModelScope.launch {
|
||||
_dateId
|
||||
.filterNotNull()
|
||||
.collect { id ->
|
||||
val params = GetObjectRelationListById.Params(
|
||||
space = vmParams.spaceId,
|
||||
value = id
|
||||
)
|
||||
Timber.d("Start RelationListWithValue with params: $params")
|
||||
getObjectRelationListById.async(params).fold(
|
||||
onSuccess = { result ->
|
||||
Timber.d("RelationListWithValue Success: $result")
|
||||
val items =
|
||||
result.toUiFieldsItem(storeOfRelations = storeOfRelations)
|
||||
initFieldsState(items)
|
||||
},
|
||||
onFailure = { e ->
|
||||
Timber.e(e, "RelationListWithValue Error")
|
||||
errorState.value = UiErrorState.Show(
|
||||
Reason.ErrorGettingFields(
|
||||
msg = e.message ?: "Error getting fields"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithGettingDateObject() {
|
||||
viewModelScope.launch {
|
||||
_dateId
|
||||
.filterNotNull()
|
||||
.collect { id ->
|
||||
val params = GetObject.Params(
|
||||
target = id,
|
||||
space = vmParams.spaceId
|
||||
)
|
||||
Timber.d("Start GetObject with params: $params")
|
||||
getObject.async(params).fold(
|
||||
onSuccess = { obj ->
|
||||
Timber.d("GetObject Success, obj:[$obj]")
|
||||
val timestampInSeconds =
|
||||
obj.details[id]?.getSingleValue<Double>(
|
||||
Relations.TIMESTAMP
|
||||
)?.toLong()
|
||||
if (timestampInSeconds != null) {
|
||||
_dateTimestamp.value = timestampInSeconds
|
||||
val (formattedDate, _) = dateProvider.formatTimestampToDateAndTime(
|
||||
timestamp = timestampInSeconds * 1000,
|
||||
)
|
||||
uiCalendarIconState.value = UiCalendarIconState.Visible(
|
||||
timestampInSeconds = TimestampInSeconds(timestampInSeconds)
|
||||
)
|
||||
uiHeaderState.value = UiHeaderState.Content(
|
||||
title = formattedDate
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { e -> Timber.e(e, "GetObject Error") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithGettingDateByTimestamp(timestamp: Long, action: (Struct?) -> Unit) {
|
||||
val params = GetDateObjectByTimestamp.Params(
|
||||
space = vmParams.spaceId,
|
||||
timestamp = timestamp
|
||||
)
|
||||
Timber.d("Start ObjectDateByTimestamp with params: [$params]")
|
||||
viewModelScope.launch {
|
||||
getDateObjectByTimestamp.async(params).fold(
|
||||
onSuccess = { dateObject ->
|
||||
Timber.d("ObjectDateByTimestamp Success, dateObject: [$dateObject]")
|
||||
action(dateObject)
|
||||
},
|
||||
onFailure = { e -> Timber.e(e, "ObjectDateByTimestamp Error") }
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Subscription
|
||||
private fun subscriptionId() = "date_object_subscription_${vmParams.spaceId}"
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun setupUiStateFlow() {
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
_dateId.filterNotNull(),
|
||||
_dateTimestamp.filterNotNull(),
|
||||
_activeField.filterNotNull(),
|
||||
restartSubscription
|
||||
) { dateId, timestamp, activeField, _ ->
|
||||
createSearchParams(
|
||||
dateId = dateId,
|
||||
timestamp = timestamp,
|
||||
space = vmParams.spaceId,
|
||||
itemsLimit = _itemsLimit,
|
||||
field = activeField
|
||||
)
|
||||
}
|
||||
.flatMapLatest { searchParams ->
|
||||
loadData(searchParams)
|
||||
}
|
||||
.catch {
|
||||
errorState.value = UiErrorState.Show(
|
||||
Reason.Other(it.message ?: "Error getting data")
|
||||
)
|
||||
}
|
||||
.collect { items ->
|
||||
uiObjectsListState.value = UiObjectsListState(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadData(
|
||||
searchParams: StoreSearchParams
|
||||
): Flow<List<UiObjectsListItem>> {
|
||||
|
||||
return storelessSubscriptionContainer.subscribe(searchParams)
|
||||
.onStart {
|
||||
uiContentState.value = if (_itemsLimit == DEFAULT_SEARCH_LIMIT) {
|
||||
UiContentState.InitLoading
|
||||
} else {
|
||||
UiContentState.Paging
|
||||
}
|
||||
Timber.d("Restart subscription: with params: $searchParams")
|
||||
}
|
||||
.map { objWrappers ->
|
||||
handleData(objWrappers)
|
||||
}.catch { e ->
|
||||
Timber.e("Error loading data: $e")
|
||||
errorState.value = UiErrorState.Show(
|
||||
Reason.ErrorGettingObjects(
|
||||
e.message ?: "Error getting objects"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleData(
|
||||
objWrappers: List<ObjectWrapper.Basic>
|
||||
): List<UiObjectsListItem> {
|
||||
|
||||
canPaginate.value = objWrappers.size == _itemsLimit
|
||||
|
||||
val items = objWrappers.map {
|
||||
it.toUiObjectsListItem(
|
||||
space = vmParams.spaceId,
|
||||
urlBuilder = urlBuilder,
|
||||
objectTypes = storeOfObjectTypes.getAll(),
|
||||
fieldParser = fieldParser
|
||||
)
|
||||
}
|
||||
uiContentState.value = if (items.isEmpty()) {
|
||||
UiContentState.Empty
|
||||
} else {
|
||||
UiContentState.Idle(scrollToTop = shouldScrollToTopItems).also {
|
||||
shouldScrollToTopItems = false
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private fun createSearchParams(
|
||||
dateId: Id,
|
||||
timestamp: TimeInSeconds,
|
||||
field: ActiveField,
|
||||
space: SpaceId,
|
||||
itemsLimit: Int
|
||||
): StoreSearchParams {
|
||||
val (filters, sorts) = filtersAndSortsForSearch(
|
||||
spaces = listOf(space.id),
|
||||
field = field,
|
||||
timestamp = timestamp,
|
||||
dateId = dateId
|
||||
)
|
||||
return StoreSearchParams(
|
||||
space = space,
|
||||
filters = filters,
|
||||
sorts = sorts,
|
||||
keys = defaultKeys,
|
||||
limit = itemsLimit,
|
||||
subscription = subscriptionId()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the limit for the number of items fetched and triggers data reload.
|
||||
*/
|
||||
fun updateLimit() {
|
||||
Timber.d("Update limit, canPaginate: ${canPaginate.value} uiContentState: ${uiContentState.value}")
|
||||
if (canPaginate.value && uiContentState.value is UiContentState.Idle) {
|
||||
_itemsLimit += DEFAULT_SEARCH_LIMIT
|
||||
restartSubscription.value++
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetLimit() {
|
||||
Timber.d("Reset limit")
|
||||
_itemsLimit = DEFAULT_SEARCH_LIMIT
|
||||
}
|
||||
|
||||
private fun unsubscribe() {
|
||||
viewModelScope.launch {
|
||||
storelessSubscriptionContainer.unsubscribe(listOf(subscriptionId()))
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Ui Actions
|
||||
private fun onFieldsEvent(item: UiFieldsItem, needToScroll: Boolean = false) {
|
||||
when (item) {
|
||||
is UiFieldsItem.Item -> {
|
||||
if (_activeField.value?.key == item.key) {
|
||||
val value = _activeField.value
|
||||
val activeSort = _activeField.value?.sort ?: DEFAULT_SORT_TYPE
|
||||
_activeField.value = value?.copy(
|
||||
sort = if (activeSort == DVSortType.ASC) {
|
||||
DVSortType.DESC
|
||||
} else {
|
||||
DVSortType.ASC
|
||||
}
|
||||
)
|
||||
shouldScrollToTopItems = true
|
||||
resetLimit()
|
||||
canPaginate.value = false
|
||||
uiContentState.value = Idle()
|
||||
uiObjectsListState.value = UiObjectsListState.Empty
|
||||
restartSubscription.value++
|
||||
updateHorizontalListState(selectedItem = item, needToScroll = needToScroll)
|
||||
} else {
|
||||
shouldScrollToTopItems = true
|
||||
resetLimit()
|
||||
canPaginate.value = false
|
||||
uiContentState.value = Idle()
|
||||
_activeField.value = ActiveField(
|
||||
key = item.key,
|
||||
format = item.relationFormat
|
||||
)
|
||||
restartSubscription.value++
|
||||
updateHorizontalListState(selectedItem = item, needToScroll = needToScroll)
|
||||
}
|
||||
}
|
||||
|
||||
is UiFieldsItem.Settings -> {
|
||||
val items = uiFieldsState.value.items
|
||||
uiFieldsSheetState.value = UiFieldsSheetState.Visible(
|
||||
items = items.filterIsInstance<UiFieldsItem.Item>()
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithReopeningDate(offset: Int) {
|
||||
val timestamp = _dateTimestamp.value
|
||||
if (timestamp == null) {
|
||||
Timber.w("Error getting timestamp")
|
||||
return
|
||||
}
|
||||
|
||||
val newTimestamp = timestamp + offset
|
||||
|
||||
val isValid = dateProvider.isTimestampWithinYearRange(
|
||||
timeStampInMillis = newTimestamp * 1000,
|
||||
yearRange = DATE_PICKER_YEAR_RANGE
|
||||
)
|
||||
|
||||
if (isValid) {
|
||||
proceedWithReopenDateObjectByTimestamp(
|
||||
timestamp = newTimestamp
|
||||
)
|
||||
} else {
|
||||
showDateOutOfRangeError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithCreateDoc(
|
||||
objType: ObjectWrapper.Type? = null
|
||||
) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
val params = objType?.uniqueKey.getCreateObjectParams(
|
||||
space = vmParams.spaceId,
|
||||
objType?.defaultTemplateId
|
||||
)
|
||||
viewModelScope.launch {
|
||||
createObject.async(params).fold(
|
||||
onSuccess = { result ->
|
||||
proceedWithNavigation(
|
||||
navigation = result.obj.navigation()
|
||||
)
|
||||
sendAnalyticsObjectCreateEvent(
|
||||
analytics = analytics,
|
||||
route = EventsDictionary.Routes.objDate,
|
||||
startTime = startTime,
|
||||
objType = objType ?: storeOfObjectTypes.getByKey(result.typeKey.key),
|
||||
view = EventsDictionary.View.viewHome,
|
||||
spaceParams = provideParams(space = vmParams.spaceId.id)
|
||||
)
|
||||
},
|
||||
onFailure = { e -> Timber.e(e, "Error while creating a new object") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithNavigation(navigation: OpenObjectNavigation) {
|
||||
viewModelScope.launch {
|
||||
when (navigation) {
|
||||
is OpenObjectNavigation.OpenDataView -> {
|
||||
effects.emit(
|
||||
DateObjectCommand.NavigateToSetOrCollection(
|
||||
id = navigation.target,
|
||||
space = SpaceId(navigation.space)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is OpenObjectNavigation.OpenEditor -> {
|
||||
effects.emit(
|
||||
DateObjectCommand.NavigateToEditor(
|
||||
id = navigation.target,
|
||||
space = SpaceId(navigation.space)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
Timber.e("Unexpected layout: ${navigation.layout}")
|
||||
effects.emit(DateObjectCommand.SendToast.UnexpectedLayout(navigation.layout?.name.orEmpty()))
|
||||
}
|
||||
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
effects.emit(
|
||||
DateObjectCommand.OpenChat(
|
||||
target = navigation.target,
|
||||
space = SpaceId(navigation.space)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
OpenObjectNavigation.NonValidObject -> {
|
||||
Timber.e("Object id is missing")
|
||||
}
|
||||
|
||||
is OpenObjectNavigation.OpenDataObject -> {
|
||||
effects.emit(
|
||||
DateObjectCommand.NavigateToEditor(
|
||||
id = navigation.target,
|
||||
space = SpaceId(navigation.space)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClicked(item: UiObjectsListItem) {
|
||||
Timber.d("onItemClicked: ${item.id}")
|
||||
when (item) {
|
||||
is UiObjectsListItem.Item -> {
|
||||
val layout = item.layout ?: return
|
||||
proceedWithNavigation(
|
||||
navigation = layout.navigation(
|
||||
target = item.id,
|
||||
space = vmParams.spaceId.id
|
||||
)
|
||||
)
|
||||
viewModelScope.launch {
|
||||
//sendAnalyticsAllContentResult(analytics = analytics)
|
||||
}
|
||||
}
|
||||
|
||||
is UiObjectsListItem.Loading -> {
|
||||
Timber.d("Loading item clicked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCreateObjectOfTypeClicked(objType: ObjectWrapper.Type) {
|
||||
proceedWithCreateDoc(objType)
|
||||
}
|
||||
|
||||
fun onDateEvent(event: DateEvent) {
|
||||
when (event) {
|
||||
is DateEvent.Calendar -> onCalendarEvent(event)
|
||||
is DateEvent.TopToolbar -> onTopToolbarEvent(event)
|
||||
is DateEvent.Header -> onHeaderEvent(event)
|
||||
is DateEvent.FieldsSheet -> onFieldsSheetEvent(event)
|
||||
is DateEvent.FieldsList -> onFieldsListEvent(event)
|
||||
is DateEvent.NavigationWidget -> onNavigationWidgetEvent(event)
|
||||
is DateEvent.ObjectsList -> onObjectsListEvent(event)
|
||||
is DateEvent.SyncStatusWidget -> onSyncStatusWidgetEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFieldsListEvent(event: DateEvent.FieldsList) {
|
||||
when (event) {
|
||||
DateEvent.FieldsList.OnScrolledToItemDismiss -> {
|
||||
uiFieldsState.value = uiFieldsState.value.copy(
|
||||
needToScrollTo = false
|
||||
)
|
||||
}
|
||||
is DateEvent.FieldsList.OnFieldClick -> onFieldsEvent(event.item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSyncStatusWidgetEvent(event: DateEvent.SyncStatusWidget) {
|
||||
when (event) {
|
||||
DateEvent.SyncStatusWidget.OnSyncStatusDismiss -> {
|
||||
uiSyncStatusWidgetState.value = UiSyncStatusWidgetState.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onObjectsListEvent(event: DateEvent.ObjectsList) {
|
||||
when (event) {
|
||||
DateEvent.ObjectsList.OnLoadMore -> updateLimit()
|
||||
is DateEvent.ObjectsList.OnObjectClicked -> onItemClicked(event.item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTopToolbarEvent(event: DateEvent.TopToolbar) {
|
||||
when (event) {
|
||||
is DateEvent.TopToolbar.OnCalendarClick -> {
|
||||
val timestampInSeconds = event.timestampInSeconds
|
||||
val timeInMillis = dateProvider.adjustToStartOfDayInUserTimeZone(
|
||||
timestamp = timestampInSeconds.time
|
||||
)
|
||||
val isValid = dateProvider.isTimestampWithinYearRange(
|
||||
timeStampInMillis = timeInMillis,
|
||||
yearRange = DATE_PICKER_YEAR_RANGE
|
||||
)
|
||||
if (isValid) {
|
||||
uiCalendarState.value = UiCalendarState.Calendar(
|
||||
timeInMillis = timeInMillis
|
||||
)
|
||||
} else {
|
||||
showDateOutOfRangeError()
|
||||
}
|
||||
}
|
||||
|
||||
is DateEvent.TopToolbar.OnSyncStatusClick -> {
|
||||
uiSyncStatusWidgetState.value =
|
||||
UiSyncStatusWidgetState.Visible(
|
||||
status = event.status.toSyncStatusWidgetState()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHeaderEvent(event: DateEvent.Header) {
|
||||
when (event) {
|
||||
DateEvent.Header.OnNextClick -> proceedWithReopeningDate(offset = SECONDS_IN_DAY)
|
||||
DateEvent.Header.OnPreviousClick -> proceedWithReopeningDate(offset = -SECONDS_IN_DAY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCalendarEvent(event: DateEvent.Calendar) {
|
||||
when (event) {
|
||||
is DateEvent.Calendar.OnCalendarDateSelected -> {
|
||||
uiCalendarState.value = UiCalendarState.Hidden
|
||||
val timeInMillis = event.timeInMillis
|
||||
Timber.d("Selected date in millis: [$timeInMillis]")
|
||||
if (timeInMillis == null) return
|
||||
proceedWithReopenDateObjectByTimestamp(
|
||||
timestamp = dateProvider.adjustFromStartOfDayInUserTimeZoneToUTC(
|
||||
timeInMillis = timeInMillis
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
DateEvent.Calendar.OnCalendarDismiss -> {
|
||||
uiCalendarState.value = UiCalendarState.Hidden
|
||||
}
|
||||
|
||||
DateEvent.Calendar.OnTodayClick -> {
|
||||
uiCalendarState.value = UiCalendarState.Hidden
|
||||
proceedWithReopenDateObjectByTimestamp(
|
||||
timestamp = dateProvider.getTimestampForTodayAtStartOfDay()
|
||||
)
|
||||
}
|
||||
|
||||
DateEvent.Calendar.OnTomorrowClick -> {
|
||||
uiCalendarState.value = UiCalendarState.Hidden
|
||||
proceedWithReopenDateObjectByTimestamp(
|
||||
timestamp = dateProvider.getTimestampForTomorrowAtStartOfDay()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFieldsSheetEvent(event: DateEvent.FieldsSheet) {
|
||||
when (event) {
|
||||
is DateEvent.FieldsSheet.OnFieldClick -> {
|
||||
uiFieldsSheetState.value = UiFieldsSheetState.Hidden
|
||||
onFieldsEvent(event.item, needToScroll = true)
|
||||
}
|
||||
|
||||
is DateEvent.FieldsSheet.OnSearchQueryChanged -> {
|
||||
Timber.d("Search query: ${event.query}")
|
||||
userInput.value = event.query
|
||||
}
|
||||
|
||||
DateEvent.FieldsSheet.OnSheetDismiss -> {
|
||||
uiFieldsSheetState.value = UiFieldsSheetState.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNavigationWidgetEvent(event: DateEvent.NavigationWidget) {
|
||||
when (event) {
|
||||
DateEvent.NavigationWidget.OnAddDocClick -> {
|
||||
proceedWithCreateDoc()
|
||||
}
|
||||
|
||||
DateEvent.NavigationWidget.OnAddDocLongClick -> {
|
||||
viewModelScope.launch {
|
||||
effects.emit(DateObjectCommand.TypeSelectionScreen)
|
||||
}
|
||||
}
|
||||
|
||||
DateEvent.NavigationWidget.OnBackClick -> {
|
||||
viewModelScope.launch {
|
||||
effects.emit(DateObjectCommand.Back)
|
||||
}
|
||||
}
|
||||
|
||||
DateEvent.NavigationWidget.OnBackLongClick -> {
|
||||
viewModelScope.launch {
|
||||
effects.emit(DateObjectCommand.ExitToSpaceWidgets)
|
||||
}
|
||||
}
|
||||
|
||||
DateEvent.NavigationWidget.OnGlobalSearchClick -> {
|
||||
viewModelScope.launch {
|
||||
effects.emit(DateObjectCommand.OpenGlobalSearch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Ui State
|
||||
private fun initFieldsState(relations: List<UiFieldsItem.Item>) {
|
||||
val relation = relations.getOrNull(0)
|
||||
if (relation == null) {
|
||||
Timber.e("Error getting relation")
|
||||
return
|
||||
}
|
||||
_activeField.value = ActiveField(
|
||||
key = relation.key,
|
||||
format = relation.relationFormat
|
||||
)
|
||||
restartSubscription.value++
|
||||
uiFieldsState.value = UiFieldsState(
|
||||
items = buildList {
|
||||
add(UiFieldsItem.Settings())
|
||||
addAll(relations)
|
||||
},
|
||||
selectedRelationKey = _activeField.value?.key
|
||||
)
|
||||
if (relations.isEmpty()) {
|
||||
uiContentState.value = UiContentState.Empty
|
||||
uiObjectsListState.value = UiObjectsListState.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateHorizontalListState(selectedItem: UiFieldsItem.Item, needToScroll: Boolean = false) {
|
||||
uiFieldsState.value = uiFieldsState.value.copy(
|
||||
selectedRelationKey = selectedItem.key,
|
||||
needToScrollTo = needToScroll
|
||||
)
|
||||
}
|
||||
|
||||
fun hideError() {
|
||||
errorState.value = UiErrorState.Hidden
|
||||
}
|
||||
|
||||
fun showDateOutOfRangeError() {
|
||||
viewModelScope.launch {
|
||||
errorState.emit(
|
||||
UiErrorState.Show(
|
||||
Reason.YearOutOfRange(
|
||||
min = DATE_PICKER_YEAR_RANGE.first,
|
||||
max = DATE_PICKER_YEAR_RANGE.last
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
//INITIAL STATE
|
||||
const val SECONDS_IN_DAY = 86400
|
||||
const val DEFAULT_SEARCH_LIMIT = 25
|
||||
val DEFAULT_SORT_TYPE = DVSortType.DESC
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package com.anytypeio.anytype.feature_date.viewmodel
|
||||
|
||||
import com.anytypeio.anytype.core_models.DVFilter
|
||||
import com.anytypeio.anytype.core_models.DVFilterCondition
|
||||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
|
||||
import com.anytypeio.anytype.core_models.Relation
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.TimeInSeconds
|
||||
|
||||
fun filtersAndSortsForSearch(
|
||||
dateId: Id,
|
||||
field: ActiveField,
|
||||
timestamp: TimeInSeconds,
|
||||
spaces: List<Id>
|
||||
): Pair<List<DVFilter>, List<DVSort>> {
|
||||
val filters = buildList {
|
||||
addAll(buildDeletedFilter())
|
||||
add(buildSpaceIdFilter(spaces))
|
||||
add(buildTemplateFilter())
|
||||
add(
|
||||
buildFieldFilter(
|
||||
dateObjectId = dateId,
|
||||
field = field,
|
||||
timestamp = timestamp
|
||||
)
|
||||
)
|
||||
add(buildLayoutFilter())
|
||||
}
|
||||
return filters to buildSorts(field)
|
||||
}
|
||||
|
||||
private fun buildSorts(
|
||||
field: ActiveField,
|
||||
): List<DVSort> {
|
||||
return listOf(
|
||||
DVSort(
|
||||
relationKey = field.key.key,
|
||||
type = field.sort,
|
||||
relationFormat = RelationFormat.DATE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildFieldFilter(
|
||||
dateObjectId: Id,
|
||||
field: ActiveField,
|
||||
timestamp: TimeInSeconds
|
||||
): DVFilter {
|
||||
val fieldKey = field.key.key
|
||||
return when (field.format) {
|
||||
Relation.Format.DATE -> {
|
||||
DVFilter(
|
||||
relation = fieldKey,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = timestamp.toDouble(),
|
||||
relationFormat = RelationFormat.DATE
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
DVFilter(
|
||||
relation = fieldKey,
|
||||
condition = DVFilterCondition.IN,
|
||||
value = dateObjectId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTemplateFilter(): DVFilter = DVFilter(
|
||||
relation = Relations.TYPE_UNIQUE_KEY,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = ObjectTypeUniqueKeys.TEMPLATE
|
||||
)
|
||||
|
||||
private fun buildSpaceIdFilter(spaces: List<Id>): DVFilter = DVFilter(
|
||||
relation = Relations.SPACE_ID,
|
||||
condition = DVFilterCondition.IN,
|
||||
value = spaces
|
||||
)
|
||||
|
||||
private fun buildLayoutFilter(): DVFilter = DVFilter(
|
||||
relation = Relations.LAYOUT,
|
||||
condition = DVFilterCondition.IN,
|
||||
value = SUPPORTED_DATE_OBJECT_LAYOUTS.map { it.code.toDouble() }
|
||||
)
|
||||
|
||||
private fun buildDeletedFilter(): List<DVFilter> {
|
||||
return listOf(
|
||||
DVFilter(
|
||||
relation = Relations.IS_ARCHIVED,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
),
|
||||
DVFilter(
|
||||
relation = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
),
|
||||
DVFilter(
|
||||
relation = Relations.IS_DELETED,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
),
|
||||
DVFilter(
|
||||
relation = Relations.IS_HIDDEN_DISCOVERY,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val SUPPORTED_DATE_OBJECT_LAYOUTS = listOf(
|
||||
ObjectType.Layout.SET,
|
||||
ObjectType.Layout.COLLECTION,
|
||||
|
||||
ObjectType.Layout.TODO,
|
||||
ObjectType.Layout.NOTE,
|
||||
ObjectType.Layout.BASIC,
|
||||
ObjectType.Layout.PROFILE,
|
||||
|
||||
ObjectType.Layout.PARTICIPANT,
|
||||
ObjectType.Layout.BOOKMARK,
|
||||
ObjectType.Layout.DATE,
|
||||
|
||||
ObjectType.Layout.FILE,
|
||||
ObjectType.Layout.IMAGE,
|
||||
ObjectType.Layout.VIDEO,
|
||||
ObjectType.Layout.AUDIO,
|
||||
ObjectType.Layout.PDF
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue