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

DROID-3347 Primitives | Fields value screen, design (#2111)

This commit is contained in:
Konstantin Ivanov 2025-02-19 22:37:54 +01:00 committed by GitHub
parent 880c71403d
commit 8ae50b27a2
Signed by: github
GPG key ID: B5690EEEBB952194
24 changed files with 1942 additions and 312 deletions

View file

@ -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 <T> SnapshotStateList<T>.swapList(newList: List<T>){
fun <T> SnapshotStateList<T>.swapList(newList: List<T>) {
clear()
addAll(newList)
}

View file

@ -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))
}
}
}

View file

@ -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
)
}

View file

@ -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
),
)
}

View file

@ -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)
}
}
}

View file

@ -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<TagView>
) {
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 chips 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 tags 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<TagView>,
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<Placeable>()
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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
)
)
}

View file

@ -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, youll 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."
)
}

View file

@ -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<Model>,
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))
}
}
}

View file

@ -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"
)
}
}
}

View file

@ -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<ObjectView>()
assertEquals(expected, result)
}
}

View file

@ -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(

View file

@ -1882,4 +1882,27 @@ Please provide specific details of your needs here.</string>
<string name="migration_migration_failed">Migration failed</string>
<string name="migration_error_please_free_up_space_and_run_the_process_again">Please free up space and run the process again.</string>
<string name="fields_screen_title">Fields</string>
<string name="field_text_title">Text</string>
<string name="field_text_empty">Enter</string>
<string name="field_number_title">Number</string>
<string name="field_number_empty">Enter</string>
<string name="field_date_title">Date</string>
<string name="field_date_empty">Value</string>
<string name="field_url_title">URL</string>
<string name="field_url_empty">Add</string>
<string name="field_email_title">E-mail</string>
<string name="field_email_empty">Enter</string>
<string name="field_phone_title">Phone number</string>
<string name="field_phone_empty">Enter</string>
<string name="field_select_title">Select</string>
<string name="field_select_empty">Select</string>
<string name="field_multiselect_title">Multiselect</string>
<string name="field_multiselect_empty">Select</string>
<string name="field_object_title">Object</string>
<string name="field_object_empty">Add</string>
<string name="field_file_and_media_title">File &amp; Media</string>
<string name="field_file_and_media_empty">Add</string>
<string name="field_checkbox_title">Select</string>
</resources>

View file

@ -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<ObjectView>.filterBy(text: String): List<ObjectView> =
if (text.isNotEmpty()) this.filter { it.isContainsText(text) } else this

View file

@ -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<ObjectView> {
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<ObjectView> {
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<ObjectView> {
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<ObjectView> {
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<Id> = when (this) {
is Id -> listOf(this)
is Collection<*> -> this.filterIsInstance<Id>()
is Map<*, *> -> this.values.filterIsInstance<Id>()
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
)

View file

@ -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<ObjectView> {
val result = mutableListOf<ObjectView>()
val ids : List<Id> = 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

View file

@ -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<ObjectView> {
val ids = value.values<Id>()
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,

View file

@ -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

View file

@ -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

View file

@ -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<DVFilter>.updateFormatForSubscription(relationLinks: List<RelationLink>
fun List<SimpleRelationView>.filterHiddenRelations(): List<SimpleRelationView> =
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<DVFilter>.updateFilters(updates: List<DVFilterUpdate>): List<DVFilter> {
val filters = this.toMutableList()
updates.forEach { update ->

View file

@ -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<ObjectView> {
val objects = mutableListOf<ObjectView>()
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<Id>().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<ObjectView> {
val objects = mutableListOf<ObjectView>()
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<Id>().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<ObjectWrapper.Option>,
relationKey: Key

View file

@ -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<String> = 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(

View file

@ -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'