diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt index 7cd5605154..61dbf82227 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt @@ -40,6 +40,12 @@ fun dark( } } +@Composable +fun dark(code: String): Color { + val colorTheme = ThemeColor.entries.find { it.code == code } ?: ThemeColor.DEFAULT + return dark(colorTheme) +} + @Composable fun light( color: ThemeColor @@ -57,6 +63,12 @@ fun light( ThemeColor.DEFAULT -> colorResource(id = R.color.palette_light_default) } +@Composable +fun light(code: String): Color { + val colorTheme = ThemeColor.entries.find { it.code == code } ?: ThemeColor.DEFAULT + return light(colorTheme) +} + @OptIn(ExperimentalFoundationApi::class) fun Modifier.bouncingClickable( enabled: Boolean = true, @@ -95,7 +107,7 @@ fun Modifier.bouncingClickable( ) } -fun SnapshotStateList.swapList(newList: List){ +fun SnapshotStateList.swapList(newList: List) { clear() addAll(newList) } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt new file mode 100644 index 0000000000..8c3b58924f --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt @@ -0,0 +1,165 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +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_models.Relation +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.views.Relations1 + +@Composable +fun FieldEmpty(modifier: Modifier = Modifier, title: String, fieldFormat: RelationFormat) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + when (fieldFormat) { + Relation.Format.LONG_TEXT, + Relation.Format.SHORT_TEXT, + Relation.Format.URL -> { + val emptyState = getEnterValueText(fieldFormat) + FieldVerticalEmpty( + modifier = defaultModifier, + title = title, + emptyState = emptyState + ) + } + + else -> { + val emptyState = getEnterValueText(fieldFormat) + FieldHorizontalEmpty( + modifier = defaultModifier, + title = title, + emptyState = emptyState + ) + } + } +} + +@Composable +private fun FieldVerticalEmpty( + modifier: Modifier = Modifier, + title: String, + emptyState: String, +) { + Column( + modifier = modifier.padding(horizontal = 16.dp, vertical = 16.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = emptyState, + style = Relations1, + color = colorResource(id = R.color.text_tertiary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun FieldHorizontalEmpty( + modifier: Modifier = Modifier, + title: String, + emptyState: String, +) { + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = modifier + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + Text( + modifier = Modifier.widthIn(max = halfScreenWidth), + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = emptyState, + style = Relations1, + color = colorResource(id = R.color.text_tertiary) + ) + } +} + +@Composable +private fun getEnterValueText(format: RelationFormat): String { + return when (format) { + Relation.Format.LONG_TEXT, + Relation.Format.SHORT_TEXT -> stringResource(R.string.field_text_empty) + + Relation.Format.NUMBER -> stringResource(R.string.field_number_empty) + Relation.Format.DATE -> stringResource(R.string.field_date_empty) + Relation.Format.CHECKBOX -> "" + Relation.Format.URL -> stringResource(R.string.field_url_empty) + Relation.Format.EMAIL -> stringResource(R.string.field_email_empty) + Relation.Format.PHONE -> stringResource(R.string.field_phone_empty) + Relation.Format.OBJECT -> stringResource(R.string.field_object_empty) + else -> "" + } +} + +@DefaultPreviews +@Composable +fun PreviewField() { + LazyColumn( + modifier = Modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + Spacer(modifier = Modifier.height(12.dp)) + } + item { + FieldEmpty( + title = "Description", + fieldFormat = Relation.Format.LONG_TEXT + ) + } + item { + FieldEmpty( + title = "Some Number, very long long long long long fields name", + fieldFormat = Relation.Format.NUMBER + ) + } + item { + Spacer(modifier = Modifier.height(12.dp)) + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt new file mode 100644 index 0000000000..effcb16890 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt @@ -0,0 +1,95 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.views.Relations1 + +@Composable +fun FieldTypeCheckbox( + modifier: Modifier = Modifier, + title: String, + isCheck: Boolean, +) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + if (isCheck) { + Image( + painter = painterResource(id = R.drawable.ic_checkbox_checked), + contentDescription = "Checkbox", + modifier = Modifier.size(24.dp) + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_checkbox_default), + contentDescription = "Checkbox", + modifier = Modifier.size(24.dp) + ) + } + } + } +} + + +@DefaultPreviews +@Composable +fun FieldTypeCheckboxPreview() { + FieldTypeCheckbox( + title = "Creation date", + isCheck = false + ) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt new file mode 100644 index 0000000000..cf76d59870 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt @@ -0,0 +1,94 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.DayOfWeekCustom +import com.anytypeio.anytype.core_models.RelativeDate +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.getPrettyName +import com.anytypeio.anytype.core_ui.views.BodyCallout +import com.anytypeio.anytype.core_ui.views.Relations1 + +@Composable +fun FieldTypeDate( + modifier: Modifier = Modifier, + title: String, + relativeDate: RelativeDate +) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + Text( + text = relativeDate.getPrettyName(), + style = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + + +@DefaultPreviews +@Composable +fun FieldTypeDatePreview() { + FieldTypeDate( + title = "Creation date", + relativeDate = RelativeDate.Tomorrow( + initialTimeInMillis = System.currentTimeMillis(), + dayOfWeek = DayOfWeekCustom.THURSDAY + ), + ) +} diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt new file mode 100644 index 0000000000..437589b953 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt @@ -0,0 +1,248 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.views.BodyCallout +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.core_ui.views.Relations2 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.presentation.relations.ObjectRelationView +import com.anytypeio.anytype.presentation.sets.model.FileView + +@Composable +fun FieldTypeFile( + modifier: Modifier = Modifier, + fieldObject: ObjectRelationView.File +) { + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + if (fieldObject.files.size == 1) { + // If there is only one item, display the title and the item in one row. + val singleItem = fieldObject.files.first() + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = singleItem + ) + } + } + } else { + Column( + modifier = defaultModifier + ) { + Text( + modifier = Modifier.wrapContentWidth(), + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(10.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + ) { + // The first item (if present) + if (fieldObject.files.isNotEmpty()) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.files.first() + ) + } + } + // The second item (if present) + if (fieldObject.files.size > 1) { + Spacer(modifier = Modifier.width(8.dp)) + Box(modifier = Modifier.widthIn(max = halfScreenWidth)) { + if (fieldObject.files.size == 2) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.files[1] + ) + } else { + // If there are more than two items, display the second item with a "+n" suffix. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + ListWidgetObjectIcon( + icon = fieldObject.files[1].icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(4.dp)) + // The main text with an integrated suffix that occupies the remaining space. + FileNameWithSuffix( + text = fieldObject.files[1].name, + suffix = "+${fieldObject.files.size - 2}", + textStyle = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + countStyle = Relations2.copy( + color = colorResource(id = R.color.text_secondary) + ), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .padding(vertical = 2.dp) + ) + } + } + } + } + } + } + } +} + +// Helper function to display a single item: icon (if available) + text. +@Composable +internal fun ItemView(modifier: Modifier, objView: FileView) { + Row( + modifier = modifier.padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ListWidgetObjectIcon( + icon = objView.icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = objView.name, + style = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +/** + * A composable that displays a row consisting of the main text and a suffix. + * If the main text is short enough, the suffix (for example, "+n") + * will appear immediately after it; if the text is long, it will be truncated (with an ellipsis) + * to leave space for the suffix. + */ +@Composable +internal fun FileNameWithSuffix( + text: String, + suffix: String, + textStyle: TextStyle, + countStyle: TextStyle, + modifier: Modifier = Modifier +) { + val density = LocalDensity.current + + SubcomposeLayout(modifier = modifier) { constraints -> + val suffixConstraints = constraints.copy(minWidth = 0, maxWidth = Constraints.Infinity) + val suffixPlaceable = subcompose("suffix") { + Box( + modifier = Modifier.background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp), + text = suffix, + style = countStyle, + maxLines = 1, + ) + } + }.first().measure(suffixConstraints) + + // The available space for the main text is the total width minus the width of the suffix. + val availableWidthForText = (constraints.maxWidth - suffixPlaceable.width).coerceAtLeast(0) + val textConstraints = constraints.copy(minWidth = 0, maxWidth = availableWidthForText) + val textPlaceable = subcompose("text") { + Text( + text = text, + style = textStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }.first().measure(textConstraints) + + // If width constraints are specified (e.g., when using weight), use the full available width. + val finalWidth = + if (constraints.hasBoundedWidth) constraints.maxWidth else (textPlaceable.width + suffixPlaceable.width) + val height = maxOf(textPlaceable.height, suffixPlaceable.height) + layout(finalWidth, height) { + // Align content to the left. + textPlaceable.placeRelative(0, 0) + val offsetYPx = with(density) { 0.5.dp.roundToPx() } + val offsetXPx = with(density) { 8.dp.roundToPx() } + suffixPlaceable.placeRelative(textPlaceable.width + offsetXPx, offsetYPx) + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt new file mode 100644 index 0000000000..de33ff9c6d --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt @@ -0,0 +1,283 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.extensions.dark +import com.anytypeio.anytype.core_ui.extensions.light +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.core_ui.views.Relations2 +import com.anytypeio.anytype.presentation.sets.model.TagView + +@Composable +fun FieldTypeMultiSelect( + modifier: Modifier = Modifier, + title: String, + tags: List +) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + TagRow( + tags = tags, + modifier = Modifier.fillMaxWidth(), + textStyle = Relations1 + ) + } +} + +/** + * A composable that displays a single tag “chip” with text and a background. + * + * @param text The tag text. + * @param backgroundColor The chip’s background color. + * @param textStyle The [TextStyle] used for the tag text. + * @param isSingle If true, the chip is rendered in single-mode – meaning that if the chip does not fit + * in the available width, its text will be truncated (TextOverflow.Ellipsis). + * @param isOverflow If true, this chip is an overflow indicator (e.g. “+3”). + * @param modifier Modifier to be applied to the chip. + */ +@Composable +fun TagChip( + text: String, + tagColor: String, + textStyle: TextStyle, + isSingle: Boolean = false, + isOverflow: Boolean = false, + modifier: Modifier = Modifier +) { + // In single mode, we allow truncation. + Box( + modifier = modifier + .wrapContentWidth() + .background(light(tagColor), shape = RoundedCornerShape(6.dp)) + .padding(horizontal = 6.dp) + ) { + Text( + text = text, + style = textStyle, + color = dark(tagColor), + maxLines = 1, + overflow = if (isSingle) TextOverflow.Ellipsis else TextOverflow.Clip + ) + } +} + +/** + * A composable that lays out a row of tags in a single horizontal line. + * + * The behavior is as follows: + * 1. **Single tag case:** If there is only one tag, it is displayed in a row. If its intrinsic width + * exceeds the available width, the text is truncated (TextOverflow.Ellipsis). + * + * 2. **Multiple tags case:** The layout tries to display as many tags as possible in full (i.e. without truncation). + * - If a tag would be rendered with truncation, it is omitted and all remaining tags are replaced by + * an overflow chip (e.g. “+n”). + * - For example, if the first tag is short and fits but the second tag’s full width would exceed the available space, + * then only the first tag is displayed and an overflow chip shows the remaining count. + * + * @param tags The list of [Tag] objects to display. + * @param modifier Modifier to be applied to the overall layout. + * @param textStyle The [TextStyle] used for the tag text. + * @param spacing The spacing (in dp) between adjacent tags. + * @param overflowChipColor The background color for the overflow chip. + */ +@Composable +fun TagRow( + tags: List, + modifier: Modifier = Modifier, + textStyle: TextStyle, + spacing: Dp = 4.dp, + overflowChipColor: Color = Color.Red +) { + val density = LocalDensity.current + + SubcomposeLayout( + modifier = modifier + .fillMaxWidth() + .wrapContentSize(Alignment.TopStart) + ) { constraints -> + val availableWidth = constraints.maxWidth + val spacingPx = spacing.roundToPx() + + // If there are no tags, layout an empty box. + if (tags.isEmpty()) { + return@SubcomposeLayout layout(0, 0) {} + } + + // --- Single tag case --- + if (tags.size == 1) { + // Render the single tag in "single" mode so that it truncates if needed. + val tagPlaceable = subcompose("tag0") { + TagChip( + modifier = Modifier.padding(horizontal = 4.dp), + text = tags[0].tag, + tagColor = tags[0].color, + textStyle = textStyle, + isSingle = true + ) + }.first().measure(constraints) + return@SubcomposeLayout layout( + width = availableWidth, + height = tagPlaceable.height + ) { + tagPlaceable.placeRelative(0, 0) + } + } + + + // --- Multiple tags case --- + val measuredPlaceables = mutableListOf() + var consumedWidth = 0 + var shownTagCount = 0 + + // Iterate over tags and measure their full intrinsic width (i.e. no truncation). + for ((index, tag) in tags.withIndex()) { + // Measure the tag chip with an "unbounded" width to get its full intrinsic width. + val tagPlaceable = subcompose("tag$index") { + TagChip( + text = tags[index].tag, + tagColor = tags[index].color, + textStyle = textStyle, + isSingle = false + ) + }.first().measure(constraints.copy(maxWidth = Constraints.Infinity)) + + // Calculate additional spacing (if not the first tag). + val additionalSpacing = if (shownTagCount > 0) spacingPx else 0 + + // How many tags would remain if we add this tag? + val remainingCount = tags.size - (shownTagCount + 1) + // Pre-measure an overflow chip if needed, using a unique key. + val overflowPlaceableCandidate = if (remainingCount > 0) { + subcompose("overflow_$index") { + TagChip( + text = "+$remainingCount", + tagColor = tags[index].color, + textStyle = textStyle, + isOverflow = true + ) + }.first().measure(constraints.copy(maxWidth = Constraints.Infinity)) + } else { + null + } + + // Compute candidate width: current consumed width + spacing + tag width + + // (if needed, spacing and overflow chip width) + val candidateWidth = consumedWidth + + additionalSpacing + + tagPlaceable.width + + (if (overflowPlaceableCandidate != null) spacingPx + overflowPlaceableCandidate.width else 0) + + // If the candidate width fits into the available width, accept this tag. + if (candidateWidth <= availableWidth) { + measuredPlaceables.add(tagPlaceable) + consumedWidth += additionalSpacing + tagPlaceable.width + shownTagCount++ + } else { + // Otherwise, do not include this tag; break out of the loop. + break + } + } + + // Calculate the number of remaining tags. + val remainingCount = tags.size - shownTagCount + val overflowPlaceable = if (remainingCount > 0) { + subcompose("overflow_final") { + Box( + modifier = Modifier.background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp), + text = "+$remainingCount", + style = Relations2.copy( + color = colorResource(id = R.color.text_secondary) + ), + maxLines = 1, + ) + } + }.first().measure(constraints.copy(maxWidth = Constraints.Infinity)) + } else { + null + } + + // Final width is the sum of consumed width plus spacing and overflow chip (if present) + val totalWidth = if (overflowPlaceable != null) { + consumedWidth + spacingPx + overflowPlaceable.width + } else { + consumedWidth + } + val maxHeight = + (measuredPlaceables.map { it.height } + listOf(overflowPlaceable?.height ?: 0)) + .maxOrNull() ?: 0 + + layout(totalWidth, maxHeight) { + var xPosition = 0 + measuredPlaceables.forEach { placeable -> + placeable.placeRelative(xPosition, 0) + xPosition += placeable.width + spacingPx + } + val offsetYPx = with(density) { 1.dp.roundToPx() } + overflowPlaceable?.placeRelative(xPosition, offsetYPx) + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt new file mode 100644 index 0000000000..4bbb96c351 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt @@ -0,0 +1,249 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.views.BodyCallout +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.core_ui.views.Relations2 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.presentation.relations.ObjectRelationView +import com.anytypeio.anytype.presentation.sets.model.ObjectView + +/** + * The main composable for FieldObject. + * + * The first item is displayed in a Box with its width constrained to half the screen width. + * + * If a second item exists: + * - If there are exactly two items, it is displayed normally. + * - If there are more than two items, the second item is displayed with a suffix "+n" + * (where n = total number of items minus two) immediately following its text. + * If the text of the second item is long, it is truncated so that the suffix is always visible. + */ +@Composable +fun FieldTypeObject( + modifier: Modifier = Modifier, + fieldObject: ObjectRelationView.Object +) { + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + if (fieldObject.objects.size == 1) { + // If there is only one item, display the title and the item in one row. + val singleItem = fieldObject.objects.first() + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = singleItem + ) + } + } + } else { + Column( + modifier = defaultModifier + ) { + Text( + modifier = Modifier.wrapContentWidth(), + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(10.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + ) { + // The first item (if present) + if (fieldObject.objects.isNotEmpty()) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.objects.first() + ) + } + } + // The second item (if present) + if (fieldObject.objects.size > 1) { + Spacer(modifier = Modifier.width(8.dp)) + Box(modifier = Modifier.widthIn(max = halfScreenWidth)) { + if (fieldObject.objects.size == 2) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.objects[1] + ) + } else { + // If there are more than two items, display the second item with a "+n" suffix. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + ListWidgetObjectIcon( + icon = fieldObject.objects[1].icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(4.dp)) + // The main text with an integrated suffix that occupies the remaining space. + TextWithSuffix( + text = fieldObject.objects[1].name, + suffix = "+${fieldObject.objects.size - 2}", + textStyle = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + countStyle = Relations2.copy( + color = colorResource(id = R.color.text_secondary) + ), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .padding(vertical = 2.dp) + ) + } + } + } + } + } + } + } +} + +// Helper function to display a single item: icon (if available) + text. +@Composable +internal fun ItemView(modifier: Modifier, objView: ObjectView) { + Row( + modifier = modifier.padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ListWidgetObjectIcon( + icon = objView.icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = objView.name, + style = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +/** + * A composable that displays a row consisting of the main text and a suffix. + * If the main text is short enough, the suffix (for example, "+n") + * will appear immediately after it; if the text is long, it will be truncated (with an ellipsis) + * to leave space for the suffix. + */ +@Composable +fun TextWithSuffix( + text: String, + suffix: String, + textStyle: TextStyle, + countStyle: TextStyle, + modifier: Modifier = Modifier +) { + val density = LocalDensity.current + + SubcomposeLayout(modifier = modifier) { constraints -> + val suffixConstraints = constraints.copy(minWidth = 0, maxWidth = Constraints.Infinity) + val suffixPlaceable = subcompose("suffix") { + Box( + modifier = Modifier.background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp), + text = suffix, + style = countStyle, + maxLines = 1, + ) + } + }.first().measure(suffixConstraints) + + // The available space for the main text is the total width minus the width of the suffix. + val availableWidthForText = (constraints.maxWidth - suffixPlaceable.width).coerceAtLeast(0) + val textConstraints = constraints.copy(minWidth = 0, maxWidth = availableWidthForText) + val textPlaceable = subcompose("text") { + Text( + text = text, + style = textStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }.first().measure(textConstraints) + + // If width constraints are specified (e.g., when using weight), use the full available width. + val finalWidth = + if (constraints.hasBoundedWidth) constraints.maxWidth else (textPlaceable.width + suffixPlaceable.width) + val height = maxOf(textPlaceable.height, suffixPlaceable.height) + layout(finalWidth, height) { + // Align content to the left. + textPlaceable.placeRelative(0, 0) + val offsetYPx = with(density) { 0.5.dp.roundToPx() } + val offsetXPx = with(density) { 8.dp.roundToPx() } + suffixPlaceable.placeRelative(textPlaceable.width + offsetXPx, offsetYPx) + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt new file mode 100644 index 0000000000..e3cb4f385b --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt @@ -0,0 +1,88 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.dark +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.presentation.sets.model.StatusView + +@Composable +fun FieldTypeSelect( + modifier: Modifier = Modifier, + title: String, + status: StatusView +) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = status.status, + style = Relations1, + color = dark(status.color), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@DefaultPreviews +@Composable +fun FieldTypeSelectPreview() { + FieldTypeSelect( + title = "Status", + status = StatusView( + id = "1", + status = "In Progress", + color = ThemeColor.TEAL.code + ) + ) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt new file mode 100644 index 0000000000..c6bfab077c --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt @@ -0,0 +1,70 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.views.Relations1 + +@Composable +fun FieldTypeText( + modifier: Modifier = Modifier, + title: String, + text: String +) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + Column( + modifier = defaultModifier + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + style = Relations1, + color = colorResource(id = R.color.text_primary), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } +} + +@DefaultPreviews +@Composable +fun FieldTypeTextPreview() { + FieldTypeText( + title = "Description", + text = "Upon creating your profile, you’ll receive your very own 12 word mnemonic ‘Recovery’ phrase to protect your account. This phrase is generated on-device and represents your master key generated upon signup, similar to a Bitcoin wallet. It also prevents anyone - including Anytype - from accessing your account and decrypting your data.\n" + + "\n" + + "All data you create will be stored locally (on-device) first. We use zero-knowledge encryption, meaning that your data is encrypted before it leaves your device to sync with other devices or backup nodes." + ) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt new file mode 100644 index 0000000000..9eaedc63f8 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt @@ -0,0 +1,237 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.features.editor.holders.relations.resRelationOrigin +import com.anytypeio.anytype.core_ui.foundation.Dragger +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.Title1 +import com.anytypeio.anytype.presentation.relations.ObjectRelationView +import com.anytypeio.anytype.presentation.relations.RelationListViewModel.Model +import timber.log.Timber + +@Composable +fun FieldListScreen( + state: List, + onRelationClicked: (Model.Item) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(rememberNestedScrollInteropConnection()) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Dragger( + modifier = Modifier.padding(vertical = 6.dp) + ) + } + item { + Box( + modifier = Modifier.height(48.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(id = R.string.fields_screen_title), + style = Title1, + color = colorResource(id = R.color.text_primary), + ) + } + } + items( + count = state.size, + key = { index -> state[index].identifier }, + itemContent = { index -> + val item = state[index] + when (item) { + is Model.Item -> { + val field = item.view + when (field) { + is ObjectRelationView.Checkbox -> { + FieldTypeCheckbox( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + isCheck = field.isChecked + ) + } + + is ObjectRelationView.Date -> { + val relativeDate = field.relativeDate + if (relativeDate != null) { + FieldTypeDate( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + relativeDate = relativeDate + ) + } else { + FieldEmpty( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + fieldFormat = RelationFormat.DATE + ) + } + } + + is ObjectRelationView.Default -> { + val textValue = field.value + if (field.key == Relations.ORIGIN) { + val code = textValue?.toInt() ?: -1 + FieldTypeText( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + text = stringResource(code.resRelationOrigin()) + ) + } else { + if (textValue.isNullOrEmpty() == true) { + FieldEmpty( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + fieldFormat = RelationFormat.LONG_TEXT + ) + } else { + FieldTypeText( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + text = textValue + ) + } + } + } + + is ObjectRelationView.File -> { + if (field.files.isEmpty()) { + FieldEmpty( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + fieldFormat = RelationFormat.FILE + ) + } else { + FieldTypeFile( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + fieldObject = field + ) + } + } + + is ObjectRelationView.Object -> { + if (field.objects.isEmpty()) { + FieldEmpty( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + fieldFormat = RelationFormat.OBJECT + ) + } else { + FieldTypeObject( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + fieldObject = field + ) + } + } + + is ObjectRelationView.Status -> { + if (field.status.isEmpty()) { + FieldEmpty( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + fieldFormat = RelationFormat.STATUS + ) + } else { + FieldTypeSelect( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + status = field.status.first() + ) + } + } + + is ObjectRelationView.Tags -> { + if (field.tags.isEmpty()) { + FieldEmpty( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + fieldFormat = RelationFormat.TAG + ) + } else { + FieldTypeMultiSelect( + modifier = Modifier.noRippleThrottledClickable { + onRelationClicked(item) + }, + title = field.name, + tags = field.tags + ) + } + } + is ObjectRelationView.Links.Backlinks, + is ObjectRelationView.Links.From, + is ObjectRelationView.ObjectType.Base, + is ObjectRelationView.ObjectType.Deleted, + is ObjectRelationView.Source -> { + Timber.e("Unsupported field type: $field, shouldn't be in the fields list") + } + } + } + + Model.Section.Featured -> { + //TODO: Implement + } + + Model.Section.Other -> { + //TODO: Implement + } + + is Model.Section.TypeFrom -> { + //TODO: Implement + } + } + } + ) + item { + Spacer(modifier = Modifier.height(64.dp)) + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt new file mode 100644 index 0000000000..1d42dc9bb1 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt @@ -0,0 +1,228 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem +import com.anytypeio.anytype.presentation.sets.model.TagView + +// -------------------- +// Test Case 1: Multiple Tags (Your First Test Case) +// -------------------- +@DefaultPreviews +@Composable +fun TagsPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ), + TagView( + id = "3", + tag = "Done", + color = ThemeColor.LIME.code, + ), + TagView( + id = "4", + tag = "In Progress", + color = ThemeColor.BLUE.code, + ), + TagView( + id = "5", + tag = "Waiting", + color = ThemeColor.YELLOW.code, + ), + TagView( + id = "6", + tag = "Blocked", + color = ThemeColor.PURPLE.code, + ), + TagView( + id = "7", + tag = "Spam", + color = ThemeColor.PINK.code, + ) + ), + title = "Tag" + ) + } + } +} + +// -------------------- +// Test Case 2: Single Tag with a Very Long Name (truncated) +// -------------------- +@Preview(showBackground = true) +@Composable +fun SingleLongTagPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "This is an extremely long tag that should be truncated if it doesn't fit in the available space", + color = ThemeColor.RED.code, + ), + ), + title = "Tag" + ) + } + } +} + +// -------------------- +// Test Case 3: Single Tag with a Short Name +// -------------------- +@Preview(showBackground = true) +@Composable +fun SingleShortTagPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + ), + title = "Tag" + ) + } + } +} + +// -------------------- +// Test Case 4: Two Tags – First Tag Short, Second Tag Very Long (second omitted → overflow) +// -------------------- +@Preview(showBackground = true) +@Composable +fun TwoTagsFirstShortSecondLongPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "This is a very long tag that might not fit entirely", + color = ThemeColor.ORANGE.code, + ) + ), + title = "Tag" + ) + } + } +} + +// -------------------- +// Test Case 5: Two Short Tags (both displayed) +// -------------------- +@Preview(showBackground = true) +@Composable +fun TwoShortTagsPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ) + ), + title = "Tag" + ) + } + } +} + +// -------------------- +// Test Case 6: Three Short Tags (all displayed) +// -------------------- +@Preview(showBackground = true) +@Composable +fun ThreeShortTagsPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ), + TagView( + id = "3", + tag = "Done", + color = ThemeColor.LIME.code, + ), + ), + title = "Tag" + ) + } + } +} + +// -------------------- +// Test Case 7: Four Tags with Overflow (only some tags displayed, remainder shown as +n) +// -------------------- +@Preview(showBackground = true) +@Composable +fun FourTagsWithOverflowPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ), + TagView( + id = "3", + tag = "Done", + color = ThemeColor.LIME.code, + ), + TagView( + id = "4", + tag = "In Progress", + color = ThemeColor.BLUE.code, + ) + ), + title = "Tag" + ) + } + } +} \ No newline at end of file diff --git a/core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/navigation/LinkToObjectViewKtTest.kt b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/navigation/LinkToObjectViewKtTest.kt deleted file mode 100644 index 57a2130640..0000000000 --- a/core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/navigation/LinkToObjectViewKtTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.anytypeio.anytype.core_ui.features.navigation - -import com.anytypeio.anytype.presentation.navigation.ObjectView -import com.anytypeio.anytype.presentation.navigation.filterBy -import com.anytypeio.anytype.presentation.navigation.isContainsText -import com.anytypeio.anytype.presentation.objects.ObjectIcon -import com.anytypeio.anytype.test_utils.MockDataFactory -import org.junit.Assert.* -import org.junit.Test - -class LinkToObjectViewKtTest { - - @Test - fun `should contain text`() { - val pageLink = ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = "Subtitle first", - title = "Title first", - icon = ObjectIcon.None - ) - val text = "IRst" - - val result = pageLink.isContainsText(text) - - assertTrue(result) - } - - @Test - fun `should not contain text`() { - val pageLink = ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = "Subtitle first", - title = "Title first", - icon = ObjectIcon.None - ) - val text = "ECO" - - val result = pageLink.isContainsText(text) - - assertFalse(result) - } - - @Test - fun `should return original list`() { - val text = "same" - val list = listOf( - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString() + text, - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ), - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString(), - title = MockDataFactory.randomString() + text, - icon = ObjectIcon.None - ), - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString(), - title = MockDataFactory.randomString() + text, - icon = ObjectIcon.None - ) - ) - - val result = list.filterBy(text) - - assertEquals(list, result) - } - - @Test - fun `should return list without one item`() { - val text = "same" - val pageLink1 = ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString() + text, - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ) - val pageLink3 = ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString() + text + MockDataFactory.randomString(), - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ) - val list = listOf( - pageLink1, - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString(), - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ), - pageLink3 - ) - - val result = list.filterBy(text) - - val expected = listOf(pageLink1, pageLink3) - assertEquals(expected, result) - } - - @Test - fun `should return empty list`() { - val text = "same" - val list = listOf( - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString(), - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ), - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString(), - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ), - ObjectView( - id = MockDataFactory.randomUuid(), - subtitle = MockDataFactory.randomString(), - title = MockDataFactory.randomString(), - icon = ObjectIcon.None - ) - ) - - val result = list.filterBy(text) - - val expected = listOf() - assertEquals(expected, result) - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt index 24c663865b..d3f4d0bf2f 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt @@ -4,6 +4,7 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.MAX_SNIPPET_SIZE import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds +import com.anytypeio.anytype.core_models.ObjectView import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.RelativeDate @@ -111,6 +112,9 @@ class FieldParserImpl @Inject constructor( //region ObjectWrapper.Basic fields override fun getObjectName(objectWrapper: ObjectWrapper.Basic): String { + if (objectWrapper.isDeleted == true) { + return stringResourceProvider.getDeletedObjectTitle() + } val result = when (objectWrapper.layout) { ObjectType.Layout.DATE -> { val relativeDate = dateProvider.calculateRelativeDates( diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 7e53172d00..1eb5e75df1 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1882,4 +1882,27 @@ Please provide specific details of your needs here. Migration failed Please free up space and run the process again. + Fields + Text + Enter + Number + Enter + Date + Value + URL + Add + E-mail + Enter + Phone number + Enter + Select + Select + Multiselect + Select + Object + Add + File & Media + Add + Select + \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/ObjectView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/ObjectView.kt index 3e688242ce..33ffe2d8e8 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/ObjectView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/ObjectView.kt @@ -25,17 +25,3 @@ data class DefaultObjectView( val lastOpenedDate: Long = 0L, val isFavorite: Boolean = false ) : DefaultSearchItem - -data class ObjectView( - val id: String, - val title: String, - val subtitle: String, - val icon: ObjectIcon, - val layout: ObjectType.Layout? = null -) - -fun ObjectView.isContainsText(text: String): Boolean = title.contains(text, true) || - subtitle.contains(text, true) - -fun List.filterBy(text: String): List = - if (text.isNotEmpty()) this.filter { it.isContainsText(text) } else this diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectViewMapper.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectViewMapper.kt new file mode 100644 index 0000000000..81137ff0ab --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectViewMapper.kt @@ -0,0 +1,130 @@ +package com.anytypeio.anytype.presentation.objects + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Struct +import com.anytypeio.anytype.core_models.ext.isValidObject +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.objects.ObjectStore +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.presentation.extension.getObject +import com.anytypeio.anytype.presentation.mapper.objectIcon +import com.anytypeio.anytype.presentation.sets.model.ObjectView +import timber.log.Timber + +/** + * Mapper class for data class @see [com.anytypeio.anytype.presentation.sets.model.ObjectView] + * that represents a view of an object in case of fields value. + */ +fun Struct.buildRelationValueObjectViews( + relationKey: Key, + details: ObjectViewDetails, + builder: UrlBuilder, + fieldParser: FieldParser +): List { + return this[relationKey] + .asIdList() + .mapNotNull { id -> + details.getObject(id) + ?.takeIf { it.isValid } + ?.toObjectView(urlBuilder = builder, fieldParser = fieldParser) + } +} + +suspend fun Struct.buildObjectViews( + columnKey: Id, + store: ObjectStore, + builder: UrlBuilder, + withIcon: Boolean = true, + fieldParser: FieldParser +): List { + return this.getOrDefault(columnKey, null) + .asIdList() + .mapNotNull { id -> + val wrapper = store.get(id) + if (wrapper == null || !wrapper.isValid) { + Timber.w("Object was missing in object store: $id or was invalid") + null + } else if (wrapper.isDeleted == true) { + ObjectView.Deleted(id = id, name = fieldParser.getObjectName(wrapper)) + } else { + val icon = if (withIcon) wrapper.objectIcon(builder) else ObjectIcon.None + ObjectView.Default( + id = id, + name = fieldParser.getObjectName(wrapper), + icon = icon, + types = wrapper.type + ) + } + } +} + +suspend fun ObjectWrapper.Basic.objects( + relation: Id, + urlBuilder: UrlBuilder, + storeOfObjects: ObjectStore, + fieldParser: FieldParser +): List { + return map.getOrDefault(relation, null) + .asIdList() + .mapNotNull { id -> + storeOfObjects.get(id) + ?.takeIf { it.isValid } + ?.toObjectView(urlBuilder, fieldParser) + } +} + +suspend fun ObjectWrapper.Relation.toObjects( + value: Any?, + store: ObjectStore, + urlBuilder: UrlBuilder, + fieldParser: FieldParser +): List { + return value.asIdList().mapNotNull { id -> + val raw = store.get(id)?.map + if (raw.isNullOrEmpty() || !raw.isValidObject()) null + else { + ObjectWrapper.Basic(raw).toObjectView(urlBuilder, fieldParser) + } + } +} + +/** + * Converts any value into a list of Ids. + * Supports a single Id, a Collection (e.g. List) of Ids, or a Map whose values are Ids. + */ +private fun Any?.asIdList(): List = when (this) { + is Id -> listOf(this) + is Collection<*> -> this.filterIsInstance() + is Map<*, *> -> this.values.filterIsInstance() + else -> emptyList() +} + +/** + * Converts a Basic wrapper into an ObjectView. + * isValid check performed already in the caller function. + */ +fun ObjectWrapper.Basic.toObjectView( + urlBuilder: UrlBuilder, + fieldParser: FieldParser +): ObjectView = if (isDeleted == true) + ObjectView.Deleted(id = id, name = fieldParser.getObjectName(this)) +else toObjectViewDefault(urlBuilder, fieldParser) + +/** + * Converts a non-deleted Basic wrapper into a Default ObjectView. + * isValid check performed already in the caller function. + */ +fun ObjectWrapper.Basic.toObjectViewDefault( + urlBuilder: UrlBuilder, + fieldParser: FieldParser +): ObjectView.Default = ObjectView.Default( + id = id, + name = fieldParser.getObjectName(this), + icon = objectIcon(urlBuilder), + types = type, + isRelation = layout == ObjectType.Layout.RELATION +) \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt index 85153e7f84..0d517b6eb0 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectWrapperExtensions.kt @@ -17,7 +17,6 @@ import com.anytypeio.anytype.presentation.sets.model.FileView import com.anytypeio.anytype.presentation.sets.model.ObjectView import com.anytypeio.anytype.presentation.sets.model.StatusView import com.anytypeio.anytype.presentation.sets.model.TagView -import com.anytypeio.anytype.presentation.sets.toObjectView import timber.log.Timber suspend fun ObjectWrapper.Basic.values( @@ -324,28 +323,6 @@ suspend fun ObjectWrapper.Basic.files( return result } -suspend fun ObjectWrapper.Basic.objects( - relation: Id, - urlBuilder: UrlBuilder, - storeOfObjects: ObjectStore, - fieldParser: FieldParser -) : List { - val result = mutableListOf() - - val ids : List = when(val value = map.getOrDefault(relation, null)) { - is Id -> listOf(value) - is List<*> -> value.typeOf() - else -> emptyList() - } - ids.forEach { id -> - val wrapper = storeOfObjects.get(id) ?: return@forEach - if (wrapper.isValid) { - result.add(wrapper.toObjectView(urlBuilder, fieldParser)) - } - } - return result -} - fun ObjectWrapper.Basic.getDescriptionOrSnippet(): String? { return when (layout) { ObjectType.Layout.NOTE -> description diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectSetRenderMapper.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectSetRenderMapper.kt index 4178882213..e70a0fe95e 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectSetRenderMapper.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectSetRenderMapper.kt @@ -9,7 +9,6 @@ import com.anytypeio.anytype.core_models.DVViewer import com.anytypeio.anytype.core_models.DVViewerCardSize import com.anytypeio.anytype.core_models.DVViewerType import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relation import com.anytypeio.anytype.core_models.ext.DateParser @@ -36,7 +35,6 @@ import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.editor.editor.model.BlockView import com.anytypeio.anytype.presentation.extension.isValueRequired -import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.mapper.toCheckboxView import com.anytypeio.anytype.presentation.mapper.toDateView import com.anytypeio.anytype.presentation.mapper.toGridRecordRows @@ -47,13 +45,13 @@ import com.anytypeio.anytype.presentation.mapper.toTextView import com.anytypeio.anytype.presentation.mapper.toView import com.anytypeio.anytype.presentation.mapper.toViewerColumns import com.anytypeio.anytype.presentation.number.NumberParser +import com.anytypeio.anytype.presentation.objects.toObjects import com.anytypeio.anytype.presentation.sets.buildGalleryViews import com.anytypeio.anytype.presentation.sets.buildListViews import com.anytypeio.anytype.presentation.sets.dataViewState import com.anytypeio.anytype.presentation.sets.filter.CreateFilterView import com.anytypeio.anytype.presentation.sets.model.FilterValue import com.anytypeio.anytype.presentation.sets.model.FilterView -import com.anytypeio.anytype.presentation.sets.model.ObjectView import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView import com.anytypeio.anytype.presentation.sets.model.StatusView import com.anytypeio.anytype.presentation.sets.model.TagView @@ -472,34 +470,6 @@ suspend fun ObjectWrapper.Relation.toStatus( } } -suspend fun ObjectWrapper.Relation.toObjects( - value: Any?, - store: ObjectStore, - urlBuilder: UrlBuilder, - fieldParser: FieldParser -) : List { - val ids = value.values() - return buildList { - ids.forEach { id -> - val raw = store.get(id)?.map - if (!raw.isNullOrEmpty()) { - val wrapper = ObjectWrapper.Basic(raw) - val obj = when (isDeleted) { - true -> ObjectView.Deleted(id) - else -> ObjectView.Default( - id = id, - name = fieldParser.getObjectName(wrapper), - icon = wrapper.objectIcon(urlBuilder), - types = type, - isRelation = wrapper.layout == ObjectType.Layout.RELATION - ) - } - add(obj) - } - } - } -} - suspend fun DVFilter.toView( store: ObjectStore, relation: ObjectWrapper.Relation, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationExtensions.kt index 8c902a6e63..72bee6bd1a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationExtensions.kt @@ -10,6 +10,7 @@ import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.presentation.extension.getOptionObject import com.anytypeio.anytype.presentation.extension.hasValue import com.anytypeio.anytype.presentation.number.NumberParser +import com.anytypeio.anytype.presentation.objects.buildRelationValueObjectViews import com.anytypeio.anytype.presentation.sets.* import com.anytypeio.anytype.presentation.sets.model.ColumnView import com.anytypeio.anytype.presentation.sets.model.Viewer diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt index a584501288..3bc37182b5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationObjectExtensions.kt @@ -14,7 +14,7 @@ import com.anytypeio.anytype.presentation.extension.getOptionObject import com.anytypeio.anytype.presentation.extension.getTypeObject import com.anytypeio.anytype.presentation.number.NumberParser import com.anytypeio.anytype.presentation.sets.buildFileViews -import com.anytypeio.anytype.presentation.sets.buildRelationValueObjectViews +import com.anytypeio.anytype.presentation.objects.buildRelationValueObjectViews import com.anytypeio.anytype.presentation.sets.model.StatusView import com.anytypeio.anytype.presentation.sets.model.TagView diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt index a5d7b18350..63bd8d1fa9 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt @@ -44,8 +44,8 @@ import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.extension.getTypeObject import com.anytypeio.anytype.presentation.editor.editor.model.BlockView -import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.objects.getProperType +import com.anytypeio.anytype.presentation.objects.toObjectViewDefault import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.ObjectSetConfig.ID_KEY @@ -54,7 +54,6 @@ import com.anytypeio.anytype.presentation.relations.isSystemKey import com.anytypeio.anytype.presentation.relations.linksFeaturedRelation import com.anytypeio.anytype.presentation.relations.title import com.anytypeio.anytype.presentation.relations.view -import com.anytypeio.anytype.presentation.sets.model.ObjectView import com.anytypeio.anytype.presentation.sets.model.SimpleRelationView import com.anytypeio.anytype.presentation.sets.model.Viewer import com.anytypeio.anytype.presentation.sets.state.ObjectState @@ -297,21 +296,6 @@ fun List.updateFormatForSubscription(relationLinks: List fun List.filterHiddenRelations(): List = filter { !it.isHidden } -fun ObjectWrapper.Basic.toObjectView(urlBuilder: UrlBuilder, fieldParser: FieldParser): ObjectView = when (isDeleted) { - true -> ObjectView.Deleted(id) - else -> toObjectViewDefault(urlBuilder, fieldParser) -} - -fun ObjectWrapper.Basic.toObjectViewDefault(urlBuilder: UrlBuilder, fieldParser: FieldParser): ObjectView.Default { - return ObjectView.Default( - id = id, - name = fieldParser.getObjectName(this), - icon = this.objectIcon(builder = urlBuilder), - types = type, - isRelation = layout == ObjectType.Layout.RELATION - ) -} - fun List.updateFilters(updates: List): List { val filters = this.toMutableList() updates.forEach { update -> diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SetsExtension.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SetsExtension.kt index 89088db50a..27743eb496 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SetsExtension.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/SetsExtension.kt @@ -15,12 +15,11 @@ import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.presentation.extension.getFileObject -import com.anytypeio.anytype.presentation.extension.getObject +import com.anytypeio.anytype.presentation.objects.buildObjectViews import com.anytypeio.anytype.presentation.relations.getDateRelationFormat import com.anytypeio.anytype.presentation.sets.model.CellView import com.anytypeio.anytype.presentation.sets.model.ColumnView import com.anytypeio.anytype.presentation.sets.model.FileView -import com.anytypeio.anytype.presentation.sets.model.ObjectView import com.anytypeio.anytype.presentation.sets.model.StatusView import com.anytypeio.anytype.presentation.sets.model.TagView import com.anytypeio.anytype.presentation.sets.model.Viewer @@ -288,91 +287,6 @@ private fun ObjectWrapper.File.toView() : FileView { ) } -fun Struct.buildRelationValueObjectViews( - relationKey: Id, - details: ObjectViewDetails, - builder: UrlBuilder, - fieldParser: FieldParser -): List { - val objects = mutableListOf() - val value = this.getOrDefault(relationKey, null) - if (value is Id) { - val wrapper = details.getObject(value) - if (wrapper != null) { - objects.add(wrapper.toObjectView(urlBuilder = builder, fieldParser = fieldParser)) - } - } else if (value is List<*>) { - value.typeOf().forEach { id -> - val wrapper = details.getObject(id) - if (wrapper != null) { - objects.add(wrapper.toObjectView(urlBuilder = builder, fieldParser = fieldParser)) - } - } - } - return objects -} - -suspend fun Struct.buildObjectViews( - columnKey: Id, - store: ObjectStore, - builder: UrlBuilder, - withIcon: Boolean = true, - fieldParser: FieldParser -): List { - val objects = mutableListOf() - val value = this.getOrDefault(columnKey, null) - if (value is Id) { - val wrapper = store.get(value) - if (wrapper != null) { - if (wrapper.isDeleted == true) { - objects.add(ObjectView.Deleted(id = value)) - } else { - val icon = if (withIcon) { - wrapper.objectIcon(builder) - } else { - ObjectIcon.None - } - objects.add( - ObjectView.Default( - id = value, - name = fieldParser.getObjectName(wrapper), - icon = icon, - types = wrapper.type - ) - ) - } - } else { - Timber.w("Object was missing in object store: $value") - } - } else if (value is List<*>) { - value.typeOf().forEach { id -> - val wrapper = store.get(id) - if (wrapper != null) { - if (wrapper.isDeleted == true) { - objects.add(ObjectView.Deleted(id = id)) - } else { - val icon = if (withIcon) { - wrapper.objectIcon(builder) - } else { - ObjectIcon.None - } - objects.add( - ObjectView.Default( - id = id, - name = fieldParser.getObjectName(wrapper), - icon = icon, - types = wrapper.type - ) - ) - } - } else { - Timber.w("Object was missing in object store: $id") - } - } - } - return objects -} - fun Struct.buildTagViews( options: List, relationKey: Key diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/TagView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/TagView.kt index 3569d36398..08e1652e82 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/TagView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/model/TagView.kt @@ -11,16 +11,22 @@ data class StatusView(val id: String, val status: String, val color: String) sealed class ObjectView { abstract val id: Id + abstract val icon: ObjectIcon + abstract val name: String data class Default( override val id: String, - val name: String, - val icon: ObjectIcon, + override val name: String, + override val icon: ObjectIcon, val types: List = emptyList(), val isRelation: Boolean = false ) : ObjectView() - data class Deleted(override val id: String) : ObjectView() + data class Deleted( + override val id: String, + override val name: String, + override val icon: ObjectIcon = ObjectIcon.Deleted, + ) : ObjectView() } data class FileView( diff --git a/settings.gradle b/settings.gradle index d5aa4aa027..dc3173e98b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,8 +56,7 @@ include ':app', ':test:android-utils', ':test:utils', ':test:core-models-stub', - ':libs', - ':feature-date' + ':libs' include ':feature-ui-settings' include ':crash-reporting'