mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-2905 Primitives | Epic | Foundation for primitives (#2098)
Co-authored-by: Evgenii Kozlov <enklave.mare.balticum@protonmail.com>
This commit is contained in:
parent
88aa30d64b
commit
4bc1e060f3
153 changed files with 10877 additions and 1616 deletions
61
feature-object-type/build.gradle
Normal file
61
feature-object-type/build.gradle
Normal file
|
@ -0,0 +1,61 @@
|
|||
plugins {
|
||||
id "com.android.library"
|
||||
id "kotlin-android"
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
buildConfigField "boolean", "USE_NEW_WINDOW_INSET_API", "true"
|
||||
buildConfigField "boolean", "USE_EDGE_TO_EDGE", "true"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
|
||||
namespace 'com.anytypeio.anytype.feature_object_type'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation project(':domain')
|
||||
implementation project(':core-ui')
|
||||
implementation project(':analytics')
|
||||
implementation project(':core-models')
|
||||
implementation project(':core-utils')
|
||||
implementation project(':localization')
|
||||
implementation project(':presentation')
|
||||
implementation project(':library-emojifier')
|
||||
|
||||
compileOnly libs.javaxInject
|
||||
|
||||
implementation libs.lifecycleViewModel
|
||||
implementation libs.lifecycleRuntime
|
||||
|
||||
implementation libs.appcompat
|
||||
implementation libs.compose
|
||||
implementation libs.composeFoundation
|
||||
implementation libs.composeToolingPreview
|
||||
implementation libs.composeMaterial3
|
||||
implementation libs.composeMaterial
|
||||
implementation libs.navigationCompose
|
||||
implementation libs.composeReorderable
|
||||
|
||||
debugImplementation libs.composeTooling
|
||||
|
||||
implementation libs.timber
|
||||
|
||||
testImplementation project(':test:android-utils')
|
||||
testImplementation project(':test:utils')
|
||||
testImplementation project(":test:core-models-stub")
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.kotlinTest
|
||||
testImplementation libs.robolectric
|
||||
testImplementation libs.androidXTestCore
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.coroutineTesting
|
||||
testImplementation libs.timberJUnit
|
||||
testImplementation libs.turbine
|
||||
}
|
2
feature-object-type/src/main/AndroidManifest.xml
Normal file
2
feature-object-type/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest/>
|
|
@ -0,0 +1,52 @@
|
|||
package com.anytypeio.anytype.feature_object_type.fields
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
|
||||
sealed class FieldEvent {
|
||||
|
||||
data object OnFieldEditScreenDismiss : FieldEvent()
|
||||
data object OnAddFieldScreenDismiss : FieldEvent()
|
||||
|
||||
data class OnFieldItemClick(val item: UiFieldsListItem) : FieldEvent()
|
||||
|
||||
data class OnAddToHeaderFieldClick(
|
||||
val item: UiAddFieldItem
|
||||
) : FieldEvent()
|
||||
|
||||
data class OnAddToSidebarFieldClick(
|
||||
val item: UiAddFieldItem
|
||||
) : FieldEvent()
|
||||
|
||||
data class OnSaveButtonClicked(
|
||||
val name: String,
|
||||
val format: RelationFormat,
|
||||
val limitObjectTypes: List<Id>
|
||||
) : FieldEvent()
|
||||
|
||||
data object OnChangeTypeClick : FieldEvent()
|
||||
data object OnLimitTypesClick : FieldEvent()
|
||||
|
||||
sealed class FieldItemMenu : FieldEvent() {
|
||||
data class OnDeleteFromTypeClick(val item: UiFieldsListItem) : FieldItemMenu()
|
||||
data class OnRemoveLocalClick(val item: UiFieldsListItem) : FieldItemMenu()
|
||||
data class OnAddLocalToTypeClick(val item: UiFieldsListItem) : FieldItemMenu()
|
||||
}
|
||||
|
||||
sealed class FieldLocalInfo : FieldEvent() {
|
||||
data object OnDismiss : FieldLocalInfo()
|
||||
}
|
||||
|
||||
sealed class Section : FieldEvent() {
|
||||
data object OnAddToHeaderIconClick : Section()
|
||||
data object OnAddToSidebarIconClick : Section()
|
||||
data object OnLocalInfoClick : Section()
|
||||
}
|
||||
|
||||
sealed class DragEvent : FieldEvent() {
|
||||
data class OnMove(val fromKey: String, val toKey: String) : DragEvent()
|
||||
data object OnDragEnd : DragEvent()
|
||||
}
|
||||
|
||||
data class OnAddFieldSearchQueryChanged(val query: String) : FieldEvent()
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
package com.anytypeio.anytype.feature_object_type.fields
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
|
||||
//region Top bar
|
||||
sealed class UiFieldsTitleState {
|
||||
data object Hidden : UiFieldsTitleState()
|
||||
data class Visible(val title: String) : UiFieldsTitleState()
|
||||
}
|
||||
|
||||
sealed class UiFieldsCancelButtonState {
|
||||
data object Hidden : UiFieldsCancelButtonState()
|
||||
data object Visible : UiFieldsCancelButtonState()
|
||||
}
|
||||
|
||||
sealed class UiFieldsSaveButtonState {
|
||||
data object Hidden : UiFieldsSaveButtonState()
|
||||
data object Visible : UiFieldsSaveButtonState()
|
||||
}
|
||||
|
||||
sealed class UiFieldsEditingPanelState {
|
||||
data object Hidden : UiFieldsEditingPanelState()
|
||||
data object Visible : UiFieldsEditingPanelState()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Fields List
|
||||
data class UiFieldsListState(val items: List<UiFieldsListItem>) {
|
||||
companion object {
|
||||
val EMPTY = UiFieldsListState(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiFieldsListItem {
|
||||
abstract val id: Id
|
||||
|
||||
sealed class Item : UiFieldsListItem() {
|
||||
abstract val fieldKey: Key
|
||||
abstract val fieldTitle: String
|
||||
abstract val format: RelationFormat
|
||||
abstract val limitObjectTypes: List<UiFieldObjectItem>
|
||||
abstract val canDelete: Boolean
|
||||
abstract val isEditableField: Boolean
|
||||
|
||||
data class Draggable(
|
||||
override val id: Id,
|
||||
override val fieldKey: Key,
|
||||
override val fieldTitle: String,
|
||||
override val format: RelationFormat,
|
||||
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
|
||||
override val canDelete: Boolean,
|
||||
override val isEditableField: Boolean
|
||||
) : Item()
|
||||
|
||||
data class Local(
|
||||
override val id: Id,
|
||||
override val fieldKey: Key,
|
||||
override val fieldTitle: String,
|
||||
override val format: RelationFormat,
|
||||
override val limitObjectTypes: List<UiFieldObjectItem> = emptyList(),
|
||||
override val canDelete: Boolean = false,
|
||||
override val isEditableField: Boolean
|
||||
) : Item()
|
||||
}
|
||||
|
||||
sealed class Section : UiFieldsListItem() {
|
||||
abstract val canAdd: Boolean
|
||||
|
||||
data class Header(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_header"
|
||||
}
|
||||
}
|
||||
|
||||
data class SideBar(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_sidebar"
|
||||
}
|
||||
}
|
||||
|
||||
data class Hidden(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_hidden"
|
||||
}
|
||||
}
|
||||
|
||||
data class Local(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_local"
|
||||
}
|
||||
}
|
||||
|
||||
data class File(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_file_recommended"
|
||||
}
|
||||
}
|
||||
|
||||
data class SpaceFields(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_space_fields"
|
||||
}
|
||||
}
|
||||
|
||||
data class LibraryFields(
|
||||
override val id: Id = ID,
|
||||
override val canAdd: Boolean = false
|
||||
) : Section() {
|
||||
companion object {
|
||||
const val ID = "section_library_fields"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Edit or New Field
|
||||
|
||||
data class UiFieldObjectItem(
|
||||
val id: Id, val key: Key, val title: String, val icon: ObjectIcon
|
||||
)
|
||||
|
||||
sealed class UiFieldEditOrNewState {
|
||||
data object Hidden : UiFieldEditOrNewState()
|
||||
sealed class Visible : UiFieldEditOrNewState() {
|
||||
abstract val item: UiFieldsListItem.Item
|
||||
|
||||
data class Edit(
|
||||
override val item: UiFieldsListItem.Item
|
||||
) : Visible()
|
||||
|
||||
data class New(
|
||||
override val item: UiFieldsListItem.Item
|
||||
) : Visible()
|
||||
|
||||
data class ViewOnly(
|
||||
override val item: UiFieldsListItem.Item
|
||||
) : Visible()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region ERRORS
|
||||
sealed class UiFieldsErrorState {
|
||||
data object Hidden : UiFieldsErrorState()
|
||||
data class Show(val reason: Reason) : UiFieldsErrorState()
|
||||
|
||||
sealed class Reason {
|
||||
data class ErrorGettingObjects(val msg: String) : Reason()
|
||||
data class Other(val msg: String) : Reason()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region COMMANDS
|
||||
sealed class TypeFieldsCommand {
|
||||
data class OpenEmojiPicker(val emoji: String) : TypeFieldsCommand()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Section Local Fields Info
|
||||
sealed class UiLocalsFieldsInfoState {
|
||||
data object Hidden : UiLocalsFieldsInfoState()
|
||||
data object Visible : UiLocalsFieldsInfoState()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Add Fields screen
|
||||
sealed class UiAddFieldsScreenState {
|
||||
data object Hidden : UiAddFieldsScreenState()
|
||||
data class Visible(val items: List<UiAddFieldItem>, val addToHeader: Boolean) : UiAddFieldsScreenState()
|
||||
}
|
||||
|
||||
data class UiAddFieldItem(
|
||||
val id: Id,
|
||||
val fieldKey: Key,
|
||||
val fieldTitle: String,
|
||||
val format: RelationFormat
|
||||
)
|
||||
//endregion
|
||||
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.anytypeio.anytype.feature_object_type.fields.ui
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
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.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.extensions.simpleIcon
|
||||
import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
|
||||
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddFieldScreen(
|
||||
state: UiAddFieldsScreenState,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
var isSearchEmpty by remember { mutableStateOf(true) }
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
if (state is UiAddFieldsScreenState.Visible) {
|
||||
ModalBottomSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.nestedScroll(rememberNestedScrollInteropConnection()),
|
||||
dragHandle = { DragHandle() },
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
fieldEvent(FieldEvent.OnAddFieldScreenDismiss)
|
||||
},
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
state = lazyListState
|
||||
) {
|
||||
item {
|
||||
DefaultSearchBar(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
isSearchEmpty = it.isEmpty()
|
||||
fieldEvent(FieldEvent.OnAddFieldSearchQueryChanged(it))
|
||||
}
|
||||
}
|
||||
items(
|
||||
count = state.items.size,
|
||||
key = { index -> state.items[index].id },
|
||||
itemContent = { index ->
|
||||
val item = state.items[index]
|
||||
FieldItem(
|
||||
modifier = commonItemModifier()
|
||||
.noRippleThrottledClickable {
|
||||
if (state.addToHeader) {
|
||||
fieldEvent(
|
||||
FieldEvent.OnAddToHeaderFieldClick(
|
||||
item = item
|
||||
)
|
||||
)
|
||||
} else {
|
||||
fieldEvent(
|
||||
FieldEvent.OnAddToSidebarFieldClick(
|
||||
item = item
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
item = item
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun FieldItem(
|
||||
modifier: Modifier,
|
||||
item: UiAddFieldItem
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
val formatIcon = item.format.simpleIcon()
|
||||
if (formatIcon != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = formatIcon),
|
||||
contentDescription = "Relation format icon",
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1.0f)
|
||||
.padding(end = 16.dp),
|
||||
text = item.fieldTitle,
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun PreviewAddFieldScreen() {
|
||||
AddFieldScreen(
|
||||
state = UiAddFieldsScreenState.Visible(
|
||||
items = listOf(
|
||||
UiAddFieldItem(
|
||||
id = "1",
|
||||
fieldKey = "key",
|
||||
fieldTitle = "Title",
|
||||
format = RelationFormat.LONG_TEXT
|
||||
)
|
||||
),
|
||||
addToHeader = true
|
||||
),
|
||||
fieldEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
package com.anytypeio.anytype.feature_object_type.fields.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.extensions.getPrettyName
|
||||
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSize
|
||||
import com.anytypeio.anytype.core_ui.views.Caption1Regular
|
||||
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
|
||||
import com.anytypeio.anytype.core_ui.views.Title2
|
||||
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
|
||||
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
|
||||
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
|
||||
import com.anytypeio.anytype.feature_object_type.ui.createDummyFieldDraggableItem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditFieldScreen(
|
||||
modifier: Modifier,
|
||||
uiFieldEditOrNewState: UiFieldEditOrNewState,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
|
||||
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
dragHandle = { DragHandle() },
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = { fieldEvent(FieldEvent.OnFieldEditScreenDismiss) },
|
||||
) {
|
||||
EditFieldContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiState = uiFieldEditOrNewState,
|
||||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditFieldContent(
|
||||
modifier: Modifier,
|
||||
uiState: UiFieldEditOrNewState.Visible,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
|
||||
val field = uiState.item
|
||||
var innerValue by remember(field.fieldTitle) { mutableStateOf(field.fieldTitle) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val isEditable = field.isEditableField
|
||||
|
||||
val title = when (uiState) {
|
||||
is UiFieldEditOrNewState.Visible.Edit -> stringResource(R.string.object_type_fields_edit_field)
|
||||
is UiFieldEditOrNewState.Visible.New -> stringResource(R.string.object_type_fields_new_field)
|
||||
is UiFieldEditOrNewState.Visible.ViewOnly -> stringResource(R.string.object_type_fields_preview_field)
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
// Header title
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(44.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = Title2,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
}
|
||||
|
||||
// Name text field
|
||||
NameTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = innerValue,
|
||||
isEditable = isEditable,
|
||||
focusRequester = focusRequester,
|
||||
keyboardController = keyboardController,
|
||||
onValueChange = { innerValue = it }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Divider()
|
||||
|
||||
// Field type section
|
||||
FieldTypeSection(
|
||||
format = field.format,
|
||||
isEditable = isEditable,
|
||||
onTypeClick = { fieldEvent(FieldEvent.OnChangeTypeClick) }
|
||||
)
|
||||
Divider()
|
||||
|
||||
// Limit object types (only for OBJECT format)
|
||||
if (field.format == RelationFormat.OBJECT) {
|
||||
LimitTypesSection(
|
||||
objTypes = field.limitObjectTypes,
|
||||
isEditable = isEditable,
|
||||
onLimitTypesClick = { fieldEvent(FieldEvent.OnLimitTypesClick) }
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
if (isEditable) {
|
||||
ButtonPrimary(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResource(R.string.object_type_fields_btn_save),
|
||||
onClick = {
|
||||
fieldEvent(
|
||||
FieldEvent.OnSaveButtonClicked(
|
||||
name = innerValue,
|
||||
format = field.format,
|
||||
limitObjectTypes = field.limitObjectTypes.map { it.id }
|
||||
)
|
||||
)
|
||||
},
|
||||
size = ButtonSize.Large
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NameTextField(
|
||||
modifier: Modifier,
|
||||
value: String,
|
||||
isEditable: Boolean,
|
||||
focusRequester: FocusRequester,
|
||||
keyboardController: SoftwareKeyboardController?,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResource(id = R.string.name),
|
||||
style = Caption1Regular,
|
||||
color = colorResource(id = R.color.text_secondary)
|
||||
)
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = HeadlineHeading.copy(color = colorResource(id = R.color.text_primary)),
|
||||
singleLine = true,
|
||||
enabled = isEditable,
|
||||
cursorBrush = SolidColor(colorResource(id = R.color.text_primary)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(start = 20.dp, top = 6.dp, end = 20.dp)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { /* You can handle focus changes here if needed */ },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onValueChange(value)
|
||||
},
|
||||
decorationBox = { innerTextField ->
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.untitled),
|
||||
style = HeadlineHeading,
|
||||
color = colorResource(id = R.color.text_tertiary),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FieldTypeSection(
|
||||
format: RelationFormat,
|
||||
isEditable: Boolean,
|
||||
onTypeClick: () -> Unit
|
||||
) {
|
||||
val icon = format.simpleIcon()
|
||||
SectionItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
text = stringResource(id = R.string.type)
|
||||
)
|
||||
Divider()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
.noRippleThrottledClickable { if (isEditable) onTypeClick() }
|
||||
) {
|
||||
if (icon != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = "Relation format icon"
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 34.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
text = stringResource(format.getPrettyName()),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (isEditable) {
|
||||
Image(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
painter = painterResource(id = R.drawable.ic_arrow_forward_24),
|
||||
contentDescription = "Change field format icon"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LimitTypesSection(
|
||||
objTypes: List<UiFieldObjectItem>,
|
||||
isEditable: Boolean,
|
||||
onLimitTypesClick: () -> Unit
|
||||
) {
|
||||
val size = objTypes.size
|
||||
SectionItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
text = stringResource(id = R.string.limit_object_types)
|
||||
)
|
||||
Divider()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
.noRippleThrottledClickable { if (isEditable) onLimitTypesClick() }
|
||||
) {
|
||||
if (objTypes.isNotEmpty()) {
|
||||
Row(modifier = Modifier.align(Alignment.CenterStart)) {
|
||||
ListWidgetObjectIcon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
icon = objTypes.first().icon,
|
||||
backgroundColor = R.color.transparent_black
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
text = objTypes.first().title.take(20),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (size > 1) {
|
||||
Text(
|
||||
text = " +${size - 1}",
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterStart),
|
||||
text = stringResource(R.string.none),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (isEditable) {
|
||||
Image(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
painter = painterResource(id = R.drawable.ic_arrow_forward_24),
|
||||
contentDescription = "Change field object types icon"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionItem(modifier: Modifier, text: String) {
|
||||
Box(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp, bottom = 8.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
text = text,
|
||||
style = Caption1Regular,
|
||||
color = colorResource(id = R.color.text_secondary)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
private fun MyPreview() {
|
||||
EditFieldScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.Edit(
|
||||
item = createDummyFieldDraggableItem()
|
||||
),
|
||||
fieldEvent = {}
|
||||
)
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
private fun MyPreviewOnlyPreview() {
|
||||
EditFieldScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.ViewOnly(
|
||||
item = createDummyFieldDraggableItem(
|
||||
isEditableField = false
|
||||
)
|
||||
),
|
||||
fieldEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package com.anytypeio.anytype.feature_object_type.fields.ui
|
||||
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSize
|
||||
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
|
||||
import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle
|
||||
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SectionLocalFieldsInfo(
|
||||
modifier: Modifier,
|
||||
state: UiLocalsFieldsInfoState,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
if (state is UiLocalsFieldsInfoState.Visible) {
|
||||
LocalInfoScreen(
|
||||
modifier = modifier,
|
||||
bottomSheetState = bottomSheetState,
|
||||
onDismiss = {
|
||||
fieldEvent(FieldEvent.FieldLocalInfo.OnDismiss)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LocalInfoScreen(
|
||||
modifier: Modifier,
|
||||
bottomSheetState: SheetState,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
dragHandle = { DragHandle() },
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = HeadlineHeading,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
text = stringResource(R.string.object_type_fields_local_info_title)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(7.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BodyCalloutRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
text = stringResource(R.string.object_type_fields_local_info_description)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
ButtonSecondary(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResource(R.string.object_type_fields_local_info_button),
|
||||
size = ButtonSize.LargeSecondary,
|
||||
onClick = {
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun SectionLocalFieldsInfoPreview() {
|
||||
SectionLocalFieldsInfo(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = UiLocalsFieldsInfoState.Visible,
|
||||
fieldEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,775 @@
|
|||
package com.anytypeio.anytype.feature_object_type.fields.ui
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedback
|
||||
import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedbackType
|
||||
import com.anytypeio.anytype.core_ui.common.rememberReorderHapticFeedback
|
||||
import com.anytypeio.anytype.core_ui.extensions.simpleIcon
|
||||
import com.anytypeio.anytype.core_ui.foundation.Dragger
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium
|
||||
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.views.Caption1Medium
|
||||
import com.anytypeio.anytype.core_ui.views.Title1
|
||||
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
|
||||
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent
|
||||
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.*
|
||||
import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.FieldItemMenu.*
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
import kotlinx.coroutines.delay
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.ReorderableLazyListState
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun FieldsMainScreen(
|
||||
uiFieldsListState: UiFieldsListState,
|
||||
uiTitleState: UiTitleState,
|
||||
uiIconState: UiIconState,
|
||||
uiFieldEditOrNewState: UiFieldEditOrNewState,
|
||||
uiFieldLocalInfoState: UiLocalsFieldsInfoState,
|
||||
uiAddFieldsScreenState: UiAddFieldsScreenState,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
|
||||
val hapticFeedback = rememberReorderHapticFeedback()
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
fieldEvent(DragEvent.OnMove(from.key as String, to.key as String))
|
||||
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
|
||||
}
|
||||
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(reorderableLazyColumnState.isAnyItemDragging) {
|
||||
if (reorderableLazyColumnState.isAnyItemDragging) {
|
||||
isDragging = true
|
||||
// Optional: Add a small delay to avoid triggering on very short drags
|
||||
delay(50)
|
||||
} else if (isDragging) {
|
||||
isDragging = false
|
||||
fieldEvent(DragEvent.OnDragEnd)
|
||||
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.MOVE)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.nestedScroll(rememberNestedScrollInteropConnection())
|
||||
.background(
|
||||
color = colorResource(id = R.color.widget_background),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
)
|
||||
.fillMaxSize(),
|
||||
containerColor = colorResource(id = R.color.transparent_black),
|
||||
contentColor = colorResource(id = R.color.widget_background),
|
||||
topBar = {
|
||||
TopBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiTitleState = uiTitleState,
|
||||
uiIconState = uiIconState
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
val contentModifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
|
||||
Modifier
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.fillMaxSize()
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = contentModifier,
|
||||
state = lazyListState
|
||||
) {
|
||||
items(
|
||||
count = uiFieldsListState.items.size,
|
||||
key = { index -> uiFieldsListState.items[index].id },
|
||||
contentType = { index -> getContentType(uiFieldsListState.items[index]) },
|
||||
itemContent = { index ->
|
||||
val item = uiFieldsListState.items[index]
|
||||
when (item) {
|
||||
is UiFieldsListItem.Item.Draggable -> {
|
||||
FieldItemDraggable(
|
||||
modifier = commonItemModifier(),
|
||||
item = item,
|
||||
reorderingState = reorderableLazyColumnState,
|
||||
fieldEvent = fieldEvent,
|
||||
hapticFeedback = hapticFeedback
|
||||
)
|
||||
}
|
||||
|
||||
is UiFieldsListItem.Item.Local -> {
|
||||
FieldItemLocal(
|
||||
modifier = commonItemModifier(),
|
||||
item = item,
|
||||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
|
||||
is Section.SideBar -> {
|
||||
SectionItem(
|
||||
item = item,
|
||||
reorderingState = reorderableLazyColumnState,
|
||||
fieldEvent = fieldEvent,
|
||||
isReorderable = true,
|
||||
onAddIconClick = {
|
||||
fieldEvent(FieldEvent.Section.OnAddToSidebarIconClick)
|
||||
}
|
||||
)
|
||||
}
|
||||
is Section.Hidden -> {
|
||||
SectionItem(
|
||||
item = item,
|
||||
reorderingState = reorderableLazyColumnState,
|
||||
fieldEvent = fieldEvent,
|
||||
isReorderable = true
|
||||
)
|
||||
}
|
||||
is Section.Header -> {
|
||||
SectionItem(
|
||||
item = item,
|
||||
reorderingState = reorderableLazyColumnState,
|
||||
fieldEvent = fieldEvent,
|
||||
isReorderable = false,
|
||||
onAddIconClick = {
|
||||
fieldEvent(FieldEvent.Section.OnAddToHeaderIconClick)
|
||||
}
|
||||
)
|
||||
}
|
||||
is Section.Local,
|
||||
is Section.File -> {
|
||||
SectionItem(
|
||||
item = item,
|
||||
reorderingState = reorderableLazyColumnState,
|
||||
fieldEvent = fieldEvent,
|
||||
isReorderable = false
|
||||
)
|
||||
}
|
||||
|
||||
is Section.LibraryFields -> TODO()
|
||||
is Section.SpaceFields -> TODO()
|
||||
}
|
||||
}
|
||||
)
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) {
|
||||
EditFieldScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiFieldEditOrNewState = uiFieldEditOrNewState,
|
||||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (uiFieldLocalInfoState is UiLocalsFieldsInfoState.Visible) {
|
||||
SectionLocalFieldsInfo(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = uiFieldLocalInfoState,
|
||||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (uiAddFieldsScreenState is UiAddFieldsScreenState.Visible) {
|
||||
AddFieldScreen(
|
||||
state = uiAddFieldsScreenState,
|
||||
fieldEvent = fieldEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a content type string based on the item type. **/
|
||||
private fun getContentType(item: UiFieldsListItem): String {
|
||||
return when (item) {
|
||||
is UiFieldsListItem.Item.Draggable -> FieldsItemsContentType.FIELD_ITEM_DRAGGABLE
|
||||
is UiFieldsListItem.Item.Local -> FieldsItemsContentType.FIELD_ITEM_LOCAL
|
||||
is Section.SideBar -> FieldsItemsContentType.SECTION_SIDEBAR
|
||||
is Section.Header -> FieldsItemsContentType.SECTION_HEADER
|
||||
is Section.Hidden -> FieldsItemsContentType.SECTION_HIDDEN
|
||||
is Section.Local -> FieldsItemsContentType.SECTION_LOCAL
|
||||
is Section.File -> FieldsItemsContentType.SECTION_FILE
|
||||
is Section.LibraryFields -> "content_type_section_library_fields"
|
||||
is Section.SpaceFields -> "content_type_section_space_fields"
|
||||
}
|
||||
}
|
||||
|
||||
/** A common modifier for list items. **/
|
||||
@Composable
|
||||
fun LazyItemScope.commonItemModifier() = Modifier
|
||||
.height(52.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
.bottomBorder()
|
||||
.animateItem()
|
||||
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
modifier: Modifier,
|
||||
uiTitleState: UiTitleState,
|
||||
uiIconState: UiIconState,
|
||||
) {
|
||||
val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
|
||||
modifier.windowInsetsPadding(WindowInsets.statusBars)
|
||||
} else {
|
||||
modifier
|
||||
}
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = colorResource(id = R.color.widget_background),
|
||||
shape = RoundedCornerShape(16.dp, 16.dp, 0.dp, 0.dp)
|
||||
)
|
||||
) {
|
||||
Dragger(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.Center),
|
||||
text = stringResource(R.string.object_type_fields_title),
|
||||
style = Title1,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
}
|
||||
InfoBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(36.dp)
|
||||
.background(color = colorResource(R.color.shape_transparent_secondary)),
|
||||
uiTitleState = uiTitleState,
|
||||
uiIconState = uiIconState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoBar(modifier: Modifier, uiTitleState: UiTitleState, uiIconState: UiIconState) {
|
||||
Row(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_fields_info_text),
|
||||
style = Caption1Medium,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
)
|
||||
ListWidgetObjectIcon(
|
||||
modifier = Modifier
|
||||
|
||||
.padding(start = 4.dp)
|
||||
.size(18.dp),
|
||||
icon = uiIconState.icon,
|
||||
backgroundColor = R.color.transparent_black
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
text = uiTitleState.title,
|
||||
style = Caption1Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyItemScope.SectionItem(
|
||||
item: UiFieldsListItem.Section,
|
||||
reorderingState: ReorderableLazyListState,
|
||||
isReorderable: Boolean = true,
|
||||
onAddIconClick: () -> Unit = {},
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
val (title, textColor) = when (item) {
|
||||
is Section.Header -> stringResource(R.string.object_type_fields_section_header) to colorResource(
|
||||
id = R.color.text_secondary
|
||||
)
|
||||
|
||||
is Section.SideBar ->
|
||||
stringResource(R.string.object_type_fields_section_fields_menu) to colorResource(
|
||||
id = R.color.text_secondary
|
||||
)
|
||||
|
||||
is Section.Hidden -> stringResource(R.string.object_type_fields_section_hidden) to colorResource(
|
||||
id = R.color.text_secondary
|
||||
)
|
||||
|
||||
is Section.Local -> stringResource(R.string.object_type_fields_section_local_fields) to colorResource(
|
||||
id = R.color.text_primary
|
||||
)
|
||||
|
||||
is Section.LibraryFields -> TODO()
|
||||
is Section.SpaceFields -> TODO()
|
||||
is Section.File -> stringResource(R.string.object_type_fields_section_file) to colorResource(
|
||||
id = R.color.text_secondary
|
||||
)
|
||||
}
|
||||
ReorderableItem(
|
||||
state = reorderingState,
|
||||
key = item.id,
|
||||
enabled = isReorderable
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 7.dp, start = 20.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
text = title,
|
||||
style = BodyCalloutMedium,
|
||||
color = textColor,
|
||||
)
|
||||
if (item.canAdd) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.width(54.dp)
|
||||
.height(40.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onAddIconClick()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 6.dp, end = 20.dp)
|
||||
.wrapContentSize()
|
||||
.align(Alignment.BottomEnd),
|
||||
painter = painterResource(R.drawable.ic_default_plus),
|
||||
contentDescription = "$title plus button"
|
||||
)
|
||||
}
|
||||
}
|
||||
if (item is Section.Local) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.height(37.dp)
|
||||
.width(44.dp)
|
||||
.noRippleThrottledClickable {
|
||||
fieldEvent(FieldEvent.Section.OnLocalInfoClick)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 9.dp, end = 20.dp)
|
||||
.wrapContentSize()
|
||||
.align(Alignment.BottomEnd),
|
||||
painter = painterResource(R.drawable.ic_section_local_fields),
|
||||
contentDescription = "Section local fields info"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun FieldItemLocal(
|
||||
modifier: Modifier,
|
||||
item: UiFieldsListItem.Item.Local,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
val isMenuExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = { fieldEvent(OnFieldItemClick(item = item)) },
|
||||
onLongClick = { isMenuExpanded.value = true }
|
||||
),
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
val formatIcon = item.format.simpleIcon()
|
||||
if (formatIcon != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = formatIcon),
|
||||
contentDescription = "Relation format icon",
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1.0f)
|
||||
.padding(end = 16.dp),
|
||||
text = item.fieldTitle,
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleThrottledClickable {
|
||||
isMenuExpanded.value = true
|
||||
},
|
||||
painter = painterResource(R.drawable.ic_space_list_dots),
|
||||
contentDescription = "Local item menu"
|
||||
)
|
||||
ItemDropDownMenu(
|
||||
item = item,
|
||||
showMenu = isMenuExpanded.value,
|
||||
onDismissRequest = {
|
||||
isMenuExpanded.value = false
|
||||
},
|
||||
onFieldEvent = {
|
||||
isMenuExpanded.value = false
|
||||
fieldEvent(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun LazyItemScope.FieldItemDraggable(
|
||||
modifier: Modifier,
|
||||
reorderingState: ReorderableLazyListState,
|
||||
hapticFeedback: ReorderHapticFeedback,
|
||||
item: UiFieldsListItem.Item.Draggable,
|
||||
fieldEvent: (FieldEvent) -> Unit
|
||||
) {
|
||||
val isMenuExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
ReorderableItem(
|
||||
state = reorderingState,
|
||||
key = item.id,
|
||||
) { isDragging ->
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
val formatIcon = item.format.simpleIcon()
|
||||
if (formatIcon != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.size(24.dp),
|
||||
painter = painterResource(id = formatIcon),
|
||||
contentDescription = "Relation format icon",
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f) // fill remaining space
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
// normal click => open/edit
|
||||
fieldEvent(OnFieldItemClick(item = item))
|
||||
},
|
||||
onLongClick = {
|
||||
// show your menu, only if NOT dragging
|
||||
if (item.canDelete) {
|
||||
isMenuExpanded.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(end = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 16.dp),
|
||||
text = item.fieldTitle,
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.draggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.START)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.END)
|
||||
}
|
||||
),
|
||||
painter = painterResource(R.drawable.ic_dnd),
|
||||
contentDescription = "Icon drag"
|
||||
)
|
||||
|
||||
ItemDropDownMenu(
|
||||
item = item,
|
||||
showMenu = isMenuExpanded.value,
|
||||
onDismissRequest = {
|
||||
isMenuExpanded.value = false
|
||||
},
|
||||
onFieldEvent = {
|
||||
isMenuExpanded.value = false
|
||||
fieldEvent(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.bottomBorder(
|
||||
strokeWidth: Dp = 0.5.dp,
|
||||
color: Color = colorResource(R.color.shape_primary)
|
||||
) = composed(
|
||||
factory = {
|
||||
val density = LocalDensity.current
|
||||
val strokeWidthPx = density.run { strokeWidth.toPx() }
|
||||
|
||||
Modifier.drawBehind {
|
||||
val width = size.width
|
||||
val height = size.height - strokeWidthPx / 2
|
||||
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x = 0f, y = height),
|
||||
end = Offset(x = width, y = height),
|
||||
strokeWidth = strokeWidthPx
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ItemDropDownMenu(
|
||||
item: UiFieldsListItem.Item,
|
||||
showMenu: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onFieldEvent: (FieldEvent) -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.width(244.dp),
|
||||
expanded = showMenu,
|
||||
offset = DpOffset(x = 0.dp, y = 0.dp),
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
},
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
) {
|
||||
when (item) {
|
||||
is UiFieldsListItem.Item.Draggable -> {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_fields_menu_delete),
|
||||
style = BodyCalloutRegular,
|
||||
color = colorResource(id = R.color.palette_system_red)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onFieldEvent(OnDeleteFromTypeClick(item))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is UiFieldsListItem.Item.Local -> {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_fields_menu_add_to_type),
|
||||
style = BodyCalloutRegular,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onFieldEvent(OnAddLocalToTypeClick(item))
|
||||
},
|
||||
)
|
||||
// DropdownMenuItem(
|
||||
// text = {
|
||||
// Text(
|
||||
// text = stringResource(R.string.object_type_fields_menu_remove),
|
||||
// style = BodyCalloutRegular,
|
||||
// color = colorResource(id = R.color.palette_system_red)
|
||||
// )
|
||||
// },
|
||||
// onClick = {
|
||||
// onFieldEvent(FieldItemMenu.OnRemoveLocalClick(item))
|
||||
// },
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun PreviewTypeFieldsMainScreen() {
|
||||
FieldsMainScreen(
|
||||
uiTitleState = UiTitleState(title = "Page", isEditable = false),
|
||||
uiIconState = UiIconState(icon = ObjectIcon.Empty.ObjectType, isEditable = false),
|
||||
uiFieldsListState = UiFieldsListState(
|
||||
items = listOf(
|
||||
UiFieldsListItem.Section.Header(),
|
||||
UiFieldsListItem.Item.Draggable(
|
||||
id = "id1",
|
||||
fieldKey = "key1",
|
||||
fieldTitle = "Status",
|
||||
format = RelationFormat.STATUS,
|
||||
canDelete = true,
|
||||
isEditableField = true
|
||||
),
|
||||
UiFieldsListItem.Item.Draggable(
|
||||
id = "id2",
|
||||
fieldKey = "key2",
|
||||
fieldTitle = "Very long field title, just to test how it looks",
|
||||
format = RelationFormat.LONG_TEXT,
|
||||
canDelete = true,
|
||||
isEditableField = true
|
||||
),
|
||||
UiFieldsListItem.Section.SideBar(
|
||||
canAdd = true
|
||||
),
|
||||
UiFieldsListItem.Item.Draggable(
|
||||
id = "id3",
|
||||
fieldKey = "key3",
|
||||
fieldTitle = "Links",
|
||||
format = RelationFormat.URL,
|
||||
isEditableField = true,
|
||||
canDelete = true
|
||||
),
|
||||
UiFieldsListItem.Item.Draggable(
|
||||
id = "id4",
|
||||
fieldKey = "key4",
|
||||
fieldTitle = "Very long field title, just to test how it looks",
|
||||
format = RelationFormat.DATE,
|
||||
isEditableField = true,
|
||||
canDelete = true
|
||||
),
|
||||
UiFieldsListItem.Section.Hidden(),
|
||||
UiFieldsListItem.Item.Draggable(
|
||||
id = "id555",
|
||||
fieldKey = "key555",
|
||||
fieldTitle = "Hidden field",
|
||||
format = RelationFormat.LONG_TEXT,
|
||||
isEditableField = true,
|
||||
canDelete = true
|
||||
),
|
||||
UiFieldsListItem.Section.Local(),
|
||||
UiFieldsListItem.Item.Local(
|
||||
id = "id5",
|
||||
fieldKey = "key5",
|
||||
fieldTitle = "Local field",
|
||||
format = RelationFormat.LONG_TEXT,
|
||||
isEditableField = true
|
||||
),
|
||||
UiFieldsListItem.Item.Local(
|
||||
id = "id6",
|
||||
fieldKey = "key6",
|
||||
fieldTitle = "Local Very long field title, just to test how it looks",
|
||||
format = RelationFormat.LONG_TEXT,
|
||||
isEditableField = true
|
||||
)
|
||||
)
|
||||
),
|
||||
fieldEvent = {},
|
||||
uiFieldEditOrNewState = UiFieldEditOrNewState.Hidden,
|
||||
uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden,
|
||||
uiAddFieldsScreenState = UiAddFieldsScreenState.Hidden
|
||||
)
|
||||
}
|
||||
|
||||
object FieldsItemsContentType {
|
||||
const val FIELD_ITEM_DRAGGABLE = "content_type_field_item_draggable"
|
||||
const val FIELD_ITEM_DEFAULT = "content_type_field_item_default"
|
||||
const val FIELD_ITEM_LOCAL = "content_type_field_item_local"
|
||||
const val SECTION_HEADER = "content_type_section_header"
|
||||
const val SECTION_SIDEBAR = "content_type_section_sidebar"
|
||||
const val SECTION_HIDDEN = "content_type_section_hidden"
|
||||
const val SECTION_LOCAL = "content_type_section_local"
|
||||
const val SECTION_FILE = "content_type_section_file"
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui
|
||||
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
|
||||
fun createDummyFieldDraggableItem(isEditableField: Boolean = true): UiFieldsListItem.Item.Draggable {
|
||||
return UiFieldsListItem.Item.Draggable(
|
||||
id = "dummyId",
|
||||
fieldKey = "dummyKey",
|
||||
fieldTitle = "Field Title",
|
||||
format = RelationFormat.OBJECT,
|
||||
limitObjectTypes = listOf(
|
||||
UiFieldObjectItem(
|
||||
id = "dummyObjectId1",
|
||||
key = "dummyKey1",
|
||||
title = "Dummy Object Type 1",
|
||||
icon = ObjectIcon.Empty.ObjectType,
|
||||
),
|
||||
UiFieldObjectItem(
|
||||
id = "dummyObjectId1",
|
||||
key = "dummyKey1",
|
||||
title = "Dummy Object Type 1",
|
||||
icon = ObjectIcon.Empty.ObjectType,
|
||||
|
||||
),
|
||||
UiFieldObjectItem(
|
||||
id = "dummyObjectId1",
|
||||
key = "dummyKey1",
|
||||
title = "Dummy Object Type 1",
|
||||
icon = ObjectIcon.Empty.ObjectType,
|
||||
|
||||
),
|
||||
),
|
||||
isEditableField = isEditableField,
|
||||
canDelete = true
|
||||
)
|
||||
}
|
|
@ -0,0 +1,462 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeKey
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.extensions.swapList
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.lists.objects.ListItemLoading
|
||||
import com.anytypeio.anytype.core_ui.lists.objects.ObjectsListItem
|
||||
import com.anytypeio.anytype.core_ui.lists.objects.UiContentState
|
||||
import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState
|
||||
import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
|
||||
import com.anytypeio.anytype.core_ui.views.ButtonSize
|
||||
import com.anytypeio.anytype.core_ui.views.Relations3
|
||||
import com.anytypeio.anytype.core_ui.views.Title2
|
||||
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.alerts.DeleteAlertScreen
|
||||
import com.anytypeio.anytype.feature_object_type.ui.header.HorizontalButtons
|
||||
import com.anytypeio.anytype.feature_object_type.ui.header.IconAndTitleWidget
|
||||
import com.anytypeio.anytype.feature_object_type.ui.header.TopToolbar
|
||||
import com.anytypeio.anytype.feature_object_type.ui.layouts.TypeLayoutsScreen
|
||||
import com.anytypeio.anytype.feature_object_type.ui.objects.ObjectsHeader
|
||||
import com.anytypeio.anytype.feature_object_type.ui.templates.TemplatesScreen
|
||||
import com.anytypeio.anytype.presentation.editor.cover.CoverColor
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
import com.anytypeio.anytype.presentation.objects.UiObjectsListItem
|
||||
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
|
||||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ObjectTypeMainScreen(
|
||||
//top bar
|
||||
uiEditButtonState: UiEditButton,
|
||||
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
|
||||
uiSyncStatusState: SyncStatusWidgetState,
|
||||
//header
|
||||
uiIconState: UiIconState,
|
||||
uiTitleState: UiTitleState,
|
||||
//layout and fields buttons
|
||||
uiFieldsButtonState: UiFieldsButtonState,
|
||||
uiLayoutButtonState: UiLayoutButtonState,
|
||||
uiLayoutTypeState: UiLayoutTypeState,
|
||||
//templates header
|
||||
uiTemplatesHeaderState: UiTemplatesHeaderState,
|
||||
uiTemplatesAddIconState: UiTemplatesAddIconState,
|
||||
//templates list
|
||||
uiTemplatesListState: UiTemplatesListState,
|
||||
//objects header
|
||||
uiObjectsHeaderState: UiObjectsHeaderState,
|
||||
uiObjectsAddIconState: UiObjectsAddIconState,
|
||||
uiObjectsSettingsIconState: UiObjectsSettingsIconState,
|
||||
uiObjectsMenuState: UiMenuState,
|
||||
//objects list
|
||||
uiObjectsListState: UiObjectsListState,
|
||||
uiContentState: UiContentState,
|
||||
//delete alert
|
||||
uiDeleteAlertState: UiDeleteAlertState,
|
||||
//events
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
|
||||
val objects = remember { mutableStateListOf<UiObjectsListItem>() }
|
||||
objects.swapList(uiObjectsListState.items)
|
||||
|
||||
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
||||
state = rememberTopAppBarState()
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
contentColor = colorResource(id = R.color.background_primary),
|
||||
topBar = {
|
||||
TopBarContent(
|
||||
uiSyncStatusBadgeState = uiSyncStatusBadgeState,
|
||||
uiEditButtonState = uiEditButtonState,
|
||||
uiTitleState = uiTitleState,
|
||||
topBarScrollBehavior = topAppBarScrollBehavior,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
MainContent(
|
||||
paddingValues = paddingValues,
|
||||
uiIconState = uiIconState,
|
||||
uiTitleState = uiTitleState,
|
||||
uiFieldsButtonState = uiFieldsButtonState,
|
||||
uiLayoutButtonState = uiLayoutButtonState,
|
||||
uiTemplatesHeaderState = uiTemplatesHeaderState,
|
||||
uiTemplatesAddIconState = uiTemplatesAddIconState,
|
||||
uiTemplatesListState = uiTemplatesListState,
|
||||
uiObjectsHeaderState = uiObjectsHeaderState,
|
||||
uiObjectsAddIconState = uiObjectsAddIconState,
|
||||
uiObjectsSettingsIconState = uiObjectsSettingsIconState,
|
||||
uiObjectsMenuState = uiObjectsMenuState,
|
||||
objects = objects,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BottomSyncStatus(
|
||||
uiSyncStatusState = uiSyncStatusState,
|
||||
onDismiss = { onTypeEvent(TypeEvent.OnSyncStatusDismiss) }
|
||||
)
|
||||
|
||||
if (uiDeleteAlertState is UiDeleteAlertState.Show) {
|
||||
DeleteAlertScreen(
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (uiLayoutTypeState is UiLayoutTypeState.Visible) {
|
||||
TypeLayoutsScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiState = uiLayoutTypeState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContent(
|
||||
paddingValues: PaddingValues,
|
||||
uiIconState: UiIconState,
|
||||
uiTitleState: UiTitleState,
|
||||
uiFieldsButtonState: UiFieldsButtonState,
|
||||
uiLayoutButtonState: UiLayoutButtonState,
|
||||
uiTemplatesHeaderState: UiTemplatesHeaderState,
|
||||
uiTemplatesAddIconState: UiTemplatesAddIconState,
|
||||
uiTemplatesListState: UiTemplatesListState,
|
||||
uiObjectsHeaderState: UiObjectsHeaderState,
|
||||
uiObjectsAddIconState: UiObjectsAddIconState,
|
||||
uiObjectsSettingsIconState: UiObjectsSettingsIconState,
|
||||
uiObjectsMenuState: UiMenuState,
|
||||
objects: List<UiObjectsListItem>,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
// Adjust content modifier based on SDK version for proper insets handling
|
||||
val contentModifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
|
||||
Modifier
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.fillMaxSize()
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
}
|
||||
|
||||
LazyColumn(modifier = contentModifier) {
|
||||
item {
|
||||
IconAndTitleWidget(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 32.dp)
|
||||
.padding(horizontal = 20.dp),
|
||||
uiIconState = uiIconState,
|
||||
uiTitleState = uiTitleState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
HorizontalButtons(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(36.dp)
|
||||
.padding(horizontal = 20.dp),
|
||||
uiFieldsButtonState = uiFieldsButtonState,
|
||||
uiLayoutButtonState = uiLayoutButtonState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (uiTemplatesHeaderState is UiTemplatesHeaderState.Visible) {
|
||||
item {
|
||||
TemplatesScreen(
|
||||
uiTemplatesHeaderState = uiTemplatesHeaderState,
|
||||
uiTemplatesAddIconState = uiTemplatesAddIconState,
|
||||
uiTemplatesListState = uiTemplatesListState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
ObjectsHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.padding(horizontal = 20.dp),
|
||||
uiObjectsHeaderState = uiObjectsHeaderState,
|
||||
uiObjectsAddIconState = uiObjectsAddIconState,
|
||||
uiObjectsSettingsIconState = uiObjectsSettingsIconState,
|
||||
uiObjectsMenuState = uiObjectsMenuState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (objects.isEmpty()) {
|
||||
item {
|
||||
EmptyScreen(
|
||||
modifier = Modifier.padding(top = 18.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(
|
||||
count = objects.size,
|
||||
key = { index -> objects[index].id },
|
||||
contentType = { index ->
|
||||
when (objects[index]) {
|
||||
is UiObjectsListItem.Loading -> "loading"
|
||||
is UiObjectsListItem.Item -> "item"
|
||||
}
|
||||
}
|
||||
) { index ->
|
||||
when (val item = objects[index]) {
|
||||
is UiObjectsListItem.Item -> {
|
||||
ObjectsListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.padding(horizontal = 4.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onTypeEvent(TypeEvent.OnObjectItemClick(item))
|
||||
},
|
||||
item = item
|
||||
)
|
||||
Divider(paddingStart = 20.dp, paddingEnd = 20.dp)
|
||||
}
|
||||
is UiObjectsListItem.Loading -> {
|
||||
ListItemLoading(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Objects menu actions
|
||||
when (val itemSet = uiObjectsMenuState.objSetItem) {
|
||||
UiMenuSetItem.CreateSet -> {
|
||||
item {
|
||||
ButtonSecondary(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, start = 20.dp, end = 20.dp),
|
||||
text = stringResource(R.string.object_type_objects_menu_create_set),
|
||||
size = ButtonSize.Large,
|
||||
onClick = { onTypeEvent(TypeEvent.OnCreateSetClick) }
|
||||
)
|
||||
}
|
||||
}
|
||||
is UiMenuSetItem.OpenSet -> {
|
||||
item {
|
||||
ButtonSecondary(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, start = 20.dp, end = 20.dp),
|
||||
text = stringResource(R.string.object_type_objects_menu_open_set),
|
||||
size = ButtonSize.Large,
|
||||
onClick = { onTypeEvent(TypeEvent.OnOpenSetClick(setId = itemSet.setId)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
UiMenuSetItem.Hidden -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBarContent(
|
||||
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
|
||||
uiEditButtonState: UiEditButton,
|
||||
uiTitleState: UiTitleState,
|
||||
topBarScrollBehavior: TopAppBarScrollBehavior,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
// Use windowInsetsPadding if running on a recent SDK
|
||||
val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) {
|
||||
Modifier
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.fillMaxWidth()
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
TopToolbar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
uiSyncStatusBadgeState = uiSyncStatusBadgeState,
|
||||
uiEditButtonState = uiEditButtonState,
|
||||
uiTitleState = uiTitleState,
|
||||
onTypeEvent = onTypeEvent,
|
||||
topBarScrollBehavior = topBarScrollBehavior
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSyncStatus(
|
||||
uiSyncStatusState: SyncStatusWidgetState,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
SpaceSyncStatusScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
modifierCard = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 16.dp),
|
||||
uiState = uiSyncStatusState,
|
||||
onDismiss = onDismiss,
|
||||
onUpdateAppClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyScreen(modifier: Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
text = stringResource(R.string.object_type_empty_items_title),
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
style = Title2,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
text = stringResource(R.string.object_type_empty_items_subtitle),
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
style = Relations3,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun ObjectTypeMainScreenPreview() {
|
||||
val spaceSyncUpdate = SpaceSyncUpdate.Update(
|
||||
id = "1",
|
||||
status = SpaceSyncStatus.SYNCING,
|
||||
network = SpaceSyncNetwork.ANYTYPE,
|
||||
error = SpaceSyncError.NULL,
|
||||
syncingObjectsCounter = 2
|
||||
)
|
||||
ObjectTypeMainScreen(
|
||||
uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible(
|
||||
status = SpaceSyncAndP2PStatusState.Success(
|
||||
spaceSyncUpdate = spaceSyncUpdate,
|
||||
p2PStatusUpdate = P2PStatusUpdate.Initial
|
||||
)
|
||||
),
|
||||
uiSyncStatusState = SyncStatusWidgetState.Hidden,
|
||||
uiIconState = UiIconState(icon = ObjectIcon.Empty.Page, isEditable = true),
|
||||
uiTitleState = UiTitleState(title = "title", isEditable = true),
|
||||
uiFieldsButtonState = UiFieldsButtonState.Visible(4),
|
||||
uiLayoutButtonState = UiLayoutButtonState.Visible(layout = ObjectType.Layout.VIDEO),
|
||||
uiTemplatesHeaderState = UiTemplatesHeaderState.Visible(count = "3"),
|
||||
uiTemplatesAddIconState = UiTemplatesAddIconState.Visible,
|
||||
uiTemplatesListState = UiTemplatesListState(
|
||||
items = listOf(
|
||||
TemplateView.Template(
|
||||
id = "1",
|
||||
name = "Template 1",
|
||||
targetTypeId = TypeId("page"),
|
||||
targetTypeKey = TypeKey("ot-page"),
|
||||
layout = ObjectType.Layout.BASIC,
|
||||
image = null,
|
||||
emoji = ":)",
|
||||
coverColor = CoverColor.RED,
|
||||
coverGradient = null,
|
||||
coverImage = null,
|
||||
),
|
||||
TemplateView.Template(
|
||||
id = "2",
|
||||
name = "Template 2",
|
||||
targetTypeId = TypeId("note"),
|
||||
targetTypeKey = TypeKey("ot-note"),
|
||||
layout = ObjectType.Layout.NOTE,
|
||||
image = null,
|
||||
emoji = null,
|
||||
coverColor = null,
|
||||
coverGradient = null,
|
||||
coverImage = null,
|
||||
),
|
||||
TemplateView.New(
|
||||
targetTypeId = TypeId("32423"),
|
||||
targetTypeKey = TypeKey("43232")
|
||||
)
|
||||
)
|
||||
),
|
||||
uiObjectsAddIconState = UiObjectsAddIconState.Visible,
|
||||
uiObjectsHeaderState = UiObjectsHeaderState(count = "3"),
|
||||
uiObjectsSettingsIconState = UiObjectsSettingsIconState.Visible,
|
||||
uiObjectsListState = UiObjectsListState(emptyList()),
|
||||
uiContentState = UiContentState.Idle(),
|
||||
uiObjectsMenuState = UiMenuState.EMPTY,
|
||||
uiDeleteAlertState = UiDeleteAlertState.Hidden,
|
||||
uiEditButtonState = UiEditButton.Visible,
|
||||
uiLayoutTypeState = UiLayoutTypeState.Hidden,
|
||||
onTypeEvent = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui
|
||||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectType.Layout
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectsListSort
|
||||
import com.anytypeio.anytype.presentation.objects.UiObjectsListItem
|
||||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
|
||||
sealed class TypeEvent {
|
||||
|
||||
//region TopBar
|
||||
data class OnSyncStatusClick(val status: SpaceSyncAndP2PStatusState) : TypeEvent()
|
||||
data object OnSyncStatusDismiss : TypeEvent()
|
||||
data object OnMenuItemDeleteClick : TypeEvent()
|
||||
data object OnAlertDeleteDismiss : TypeEvent()
|
||||
data object OnAlertDeleteConfirm : TypeEvent()
|
||||
data object OnBackClick : TypeEvent()
|
||||
//endregion
|
||||
|
||||
//region Object Type Header
|
||||
data object OnObjectTypeIconClick : TypeEvent()
|
||||
data class OnObjectTypeTitleUpdate(val title: String) : TypeEvent()
|
||||
//endregion
|
||||
|
||||
//region Sets
|
||||
data object OnCreateSetClick : TypeEvent()
|
||||
data class OnOpenSetClick(val setId: Id) : TypeEvent()
|
||||
//endregion
|
||||
|
||||
//region Objects Header
|
||||
data class OnSortClick(val sort: ObjectsListSort) : TypeEvent()
|
||||
data object OnCreateObjectIconClick : TypeEvent()
|
||||
//endregion
|
||||
|
||||
//region Objects list
|
||||
data class OnObjectItemClick(val item: UiObjectsListItem) : TypeEvent()
|
||||
//endregion
|
||||
|
||||
//region Templates
|
||||
data object OnTemplatesAddIconClick : TypeEvent()
|
||||
data class OnTemplateItemClick(val item: TemplateView) : TypeEvent()
|
||||
sealed class OnTemplateMenuClick : TypeEvent() {
|
||||
data class SetAsDefault(val item: TemplateView) : OnTemplateMenuClick()
|
||||
data class Edit(val item: TemplateView) : OnTemplateMenuClick()
|
||||
data class Duplicate(val item: TemplateView) : OnTemplateMenuClick()
|
||||
data class Delete(val item: TemplateView) : OnTemplateMenuClick()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Layout type
|
||||
data object OnLayoutTypeDismiss : TypeEvent()
|
||||
data class OnLayoutTypeItemClick(val item: Layout) : TypeEvent()
|
||||
|
||||
//endregion
|
||||
|
||||
data object OnLayoutButtonClick : TypeEvent()
|
||||
data object OnFieldsButtonClick : TypeEvent()
|
||||
|
||||
data object OnObjectsSettingsIconClick : TypeEvent()
|
||||
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.multiplayer.SpaceSyncAndP2PStatusState
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.presentation.objects.MenuSortsItem
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectsListSort
|
||||
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
|
||||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
|
||||
data class ObjectTypeVmParams(
|
||||
val objectId: Id,
|
||||
val spaceId: SpaceId,
|
||||
val withSubscriptions: Boolean,
|
||||
val showHiddenFields: Boolean
|
||||
)
|
||||
|
||||
sealed class ObjectTypeCommand {
|
||||
|
||||
data object Back : ObjectTypeCommand()
|
||||
|
||||
data class OpenTemplate(
|
||||
val templateId: Id,
|
||||
val typeId: Id,
|
||||
val typeKey: Key,
|
||||
val spaceId: Id
|
||||
) : ObjectTypeCommand()
|
||||
|
||||
data object OpenEmojiPicker : ObjectTypeCommand()
|
||||
|
||||
data object OpenFieldsScreen : ObjectTypeCommand()
|
||||
|
||||
data class OpenAddFieldScreen(val typeId: Id, val space: Id, val isSet: Boolean = false) : ObjectTypeCommand()
|
||||
}
|
||||
|
||||
//region OBJECT TYPE HEADER (title + icon)
|
||||
data class UiTitleState(val title: String, val isEditable: Boolean) {
|
||||
companion object {
|
||||
val EMPTY = UiTitleState(title = "", isEditable = false)
|
||||
}
|
||||
}
|
||||
|
||||
data class UiIconState(val icon: ObjectIcon, val isEditable: Boolean) {
|
||||
companion object {
|
||||
val EMPTY = UiIconState(icon = ObjectIcon.None, isEditable = false)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region LAYOUTS
|
||||
sealed class UiLayoutButtonState {
|
||||
data object Hidden : UiLayoutButtonState()
|
||||
data class Visible(val layout: ObjectType.Layout) : UiLayoutButtonState()
|
||||
}
|
||||
|
||||
sealed class UiLayoutTypeState {
|
||||
data object Hidden : UiLayoutTypeState()
|
||||
data class Visible(
|
||||
val layouts: List<ObjectType.Layout>,
|
||||
val selectedLayout: ObjectType.Layout? = null
|
||||
) : UiLayoutTypeState()
|
||||
}
|
||||
//endregion
|
||||
|
||||
sealed class UiFieldsButtonState {
|
||||
data object Hidden : UiFieldsButtonState()
|
||||
data class Visible(val count: Int) : UiFieldsButtonState()
|
||||
|
||||
}
|
||||
|
||||
//region MENU
|
||||
@Immutable
|
||||
sealed class UiMenuSetItem {
|
||||
data object Hidden : UiMenuSetItem()
|
||||
data object CreateSet : UiMenuSetItem()
|
||||
|
||||
@Immutable
|
||||
data class OpenSet(val setId: Id) : UiMenuSetItem()
|
||||
}
|
||||
|
||||
data class UiMenuState(
|
||||
val container: MenuSortsItem.Container,
|
||||
val sorts: List<MenuSortsItem.Sort>,
|
||||
val types: List<MenuSortsItem.SortType>,
|
||||
val objSetItem: UiMenuSetItem
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = UiMenuState(
|
||||
container = MenuSortsItem.Container(sort = ObjectsListSort.ByName()),
|
||||
sorts = emptyList(),
|
||||
types = emptyList(),
|
||||
objSetItem = UiMenuSetItem.Hidden
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class UiSettingsMenuState {
|
||||
data object Hidden : UiSettingsMenuState()
|
||||
|
||||
@Immutable
|
||||
data class Visible(
|
||||
val menuItems: List<UiSettingsMenuItem>
|
||||
) : UiSettingsMenuState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class UiTemplatesMenuState {
|
||||
data object Hidden : UiTemplatesMenuState()
|
||||
|
||||
@Immutable
|
||||
data class Visible(
|
||||
val menuItems: List<UiTemplatesMenuItem>
|
||||
) : UiTemplatesMenuState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
enum class UiSettingsMenuItem {
|
||||
DELETE
|
||||
}
|
||||
|
||||
@Immutable
|
||||
enum class UiTemplatesMenuItem {
|
||||
DELETE, DUPLICATE
|
||||
}
|
||||
|
||||
@Immutable
|
||||
enum class UiObjectsMenuItem {
|
||||
OPEN_SET, SORT_BY,
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region TEMPLATES HEADER
|
||||
sealed class UiTemplatesHeaderState {
|
||||
data object Hidden : UiTemplatesHeaderState()
|
||||
data class Visible(val count: String) : UiTemplatesHeaderState()
|
||||
}
|
||||
|
||||
sealed class UiTemplatesAddIconState {
|
||||
data object Hidden : UiTemplatesAddIconState()
|
||||
data object Visible : UiTemplatesAddIconState()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region TEMPLATES LIST
|
||||
data class UiTemplatesListState(
|
||||
val items: List<TemplateView>
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = UiTemplatesListState(items = emptyList())
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region OBJECTS HEADER
|
||||
data class UiObjectsHeaderState(val count: String) {
|
||||
companion object {
|
||||
val EMPTY = UiObjectsHeaderState(count = "")
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiObjectsAddIconState {
|
||||
data object Hidden : UiObjectsAddIconState()
|
||||
data object Visible : UiObjectsAddIconState()
|
||||
}
|
||||
|
||||
sealed class UiObjectsSettingsIconState {
|
||||
data object Hidden : UiObjectsSettingsIconState()
|
||||
data object Visible : UiObjectsSettingsIconState()
|
||||
}
|
||||
//endregion
|
||||
|
||||
sealed class UiEditButton {
|
||||
data object Hidden : UiEditButton()
|
||||
data object Visible : UiEditButton()
|
||||
}
|
||||
|
||||
//region ERRORS
|
||||
sealed class UiErrorState {
|
||||
data object Hidden : UiErrorState()
|
||||
data class Show(val reason: Reason) : UiErrorState()
|
||||
|
||||
sealed class Reason {
|
||||
data class ErrorGettingObjects(val msg: String) : Reason()
|
||||
data class Other(val msg: String) : Reason()
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region ALERTS
|
||||
sealed class UiDeleteAlertState {
|
||||
data object Hidden : UiDeleteAlertState()
|
||||
data object Show : UiDeleteAlertState()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region SYNC STATUS
|
||||
sealed class UiSyncStatusWidgetState {
|
||||
data object Hidden : UiSyncStatusWidgetState()
|
||||
data class Visible(val status: SyncStatusWidgetState) : UiSyncStatusWidgetState()
|
||||
}
|
||||
|
||||
sealed class UiSyncStatusBadgeState {
|
||||
data object Hidden : UiSyncStatusBadgeState()
|
||||
data class Visible(val status: SpaceSyncAndP2PStatusState) : UiSyncStatusBadgeState()
|
||||
}
|
||||
//endregion
|
|
@ -0,0 +1,254 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui
|
||||
|
||||
import com.anytypeio.anytype.core_models.CoverType
|
||||
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.RelationFormat
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeId
|
||||
import com.anytypeio.anytype.core_models.primitives.TypeKey
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.primitives.FieldParser
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Item
|
||||
import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section
|
||||
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
|
||||
import com.anytypeio.anytype.presentation.mapper.objectIcon
|
||||
import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper
|
||||
import com.anytypeio.anytype.presentation.relations.getCover
|
||||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
|
||||
//region Mapping
|
||||
fun ObjectWrapper.Basic.toTemplateView(
|
||||
objType: ObjectWrapper.Type,
|
||||
urlBuilder: UrlBuilder,
|
||||
coverImageHashProvider: CoverImageHashProvider,
|
||||
): TemplateView.Template {
|
||||
val coverContainer = if (coverType != CoverType.NONE) {
|
||||
BasicObjectCoverWrapper(this)
|
||||
.getCover(urlBuilder, coverImageHashProvider)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return TemplateView.Template(
|
||||
id = id,
|
||||
name = name.orEmpty(),
|
||||
targetTypeId = TypeId(targetObjectType.orEmpty()),
|
||||
emoji = if (!iconEmoji.isNullOrBlank()) iconEmoji else null,
|
||||
image = iconImage?.takeIf { it.isNotBlank() }?.let { urlBuilder.thumbnail(it) },
|
||||
layout = objType.recommendedLayout ?: ObjectType.Layout.BASIC,
|
||||
coverColor = coverContainer?.coverColor,
|
||||
coverImage = coverContainer?.coverImage,
|
||||
coverGradient = coverContainer?.coverGradient,
|
||||
isDefault = false,
|
||||
targetTypeKey = TypeKey(objType.uniqueKey)
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* Extension function to safely get a name for the relation.
|
||||
* If the name is blank, returns a default untitled title.
|
||||
*/
|
||||
private fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String =
|
||||
if (name.isNullOrBlank()) {
|
||||
stringResourceProvider.getUntitledObjectTitle()
|
||||
} else {
|
||||
name!!
|
||||
}
|
||||
|
||||
suspend fun buildUiFieldsList(
|
||||
objType: ObjectWrapper.Type,
|
||||
stringResourceProvider: StringResourceProvider,
|
||||
fieldParser: FieldParser,
|
||||
urlBuilder: UrlBuilder,
|
||||
storeOfObjectTypes: StoreOfObjectTypes,
|
||||
storeOfRelations: StoreOfRelations,
|
||||
objTypeConflictingFields: List<Id>,
|
||||
showHiddenFields: Boolean
|
||||
): List<UiFieldsListItem> {
|
||||
|
||||
val parsedFields = fieldParser.getObjectTypeParsedFields(
|
||||
objectType = objType,
|
||||
storeOfRelations = storeOfRelations,
|
||||
objectTypeConflictingFieldsIds = objTypeConflictingFields
|
||||
)
|
||||
|
||||
// The mapping functions already skip the Relations.DESCRIPTION key.
|
||||
val headerItems = parsedFields.header.mapNotNull {
|
||||
mapToUiFieldsDraggableListItem(
|
||||
field = it,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
fieldParser = fieldParser,
|
||||
urlBuilder = urlBuilder,
|
||||
storeOfObjectTypes = storeOfObjectTypes
|
||||
)
|
||||
}
|
||||
val sidebarItems = parsedFields.sidebar.mapNotNull {
|
||||
mapToUiFieldsDraggableListItem(
|
||||
field = it,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
fieldParser = fieldParser,
|
||||
urlBuilder = urlBuilder,
|
||||
storeOfObjectTypes = storeOfObjectTypes
|
||||
)
|
||||
}
|
||||
val hiddenItems = parsedFields.hidden.mapNotNull {
|
||||
mapToUiFieldsDraggableListItem(
|
||||
field = it,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
fieldParser = fieldParser,
|
||||
urlBuilder = urlBuilder,
|
||||
storeOfObjectTypes = storeOfObjectTypes
|
||||
)
|
||||
}
|
||||
val conflictedItems = parsedFields.localWithoutSystem.mapNotNull {
|
||||
mapToUiFieldsLocalListItem(
|
||||
field = it,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
fieldParser = fieldParser,
|
||||
urlBuilder = urlBuilder,
|
||||
storeOfObjectTypes = storeOfObjectTypes
|
||||
)
|
||||
}
|
||||
|
||||
//this items goes to the Hidden section as draggable items
|
||||
val conflictedSystemItems = parsedFields.localSystem.mapNotNull {
|
||||
mapToUiFieldsDraggableListItem(
|
||||
field = it,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
fieldParser = fieldParser,
|
||||
urlBuilder = urlBuilder,
|
||||
storeOfObjectTypes = storeOfObjectTypes
|
||||
)
|
||||
}
|
||||
|
||||
val fileRecommendedFields = parsedFields.file.mapNotNull {
|
||||
mapToUiFieldsDraggableListItem(
|
||||
field = it,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
fieldParser = fieldParser,
|
||||
urlBuilder = urlBuilder,
|
||||
storeOfObjectTypes = storeOfObjectTypes
|
||||
)
|
||||
}
|
||||
|
||||
return buildList {
|
||||
add(Section.Header(canAdd = true))
|
||||
addAll(headerItems)
|
||||
|
||||
add(Section.SideBar(canAdd = true))
|
||||
addAll(sidebarItems)
|
||||
|
||||
//todo file fields are off for now
|
||||
// if (fileRecommendedFields.isNotEmpty()) {
|
||||
// add(Section.File(canAdd = false))
|
||||
// addAll(fileRecommendedFields)
|
||||
// }
|
||||
|
||||
if (showHiddenFields) {
|
||||
add(Section.Hidden(canAdd = false))
|
||||
addAll(hiddenItems)
|
||||
addAll(conflictedSystemItems)
|
||||
}
|
||||
|
||||
if (conflictedItems.isNotEmpty()) {
|
||||
add(Section.Local(canAdd = false))
|
||||
addAll(conflictedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared helper to build the limit object types for a field.
|
||||
*/
|
||||
private suspend fun mapLimitObjectTypes(
|
||||
relation: ObjectWrapper.Relation,
|
||||
storeOfObjectTypes: StoreOfObjectTypes,
|
||||
fieldParser: FieldParser,
|
||||
urlBuilder: UrlBuilder
|
||||
): List<UiFieldObjectItem> {
|
||||
return if (relation.format == RelationFormat.OBJECT && relation.relationFormatObjectTypes.isNotEmpty()) {
|
||||
relation.relationFormatObjectTypes.mapNotNull { key ->
|
||||
storeOfObjectTypes.getByKey(key)?.let { objType ->
|
||||
UiFieldObjectItem(
|
||||
id = objType.id,
|
||||
key = objType.uniqueKey,
|
||||
title = fieldParser.getObjectName(objType),
|
||||
icon = objType.objectIcon(urlBuilder)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a field to a draggable UI list item.
|
||||
* Returns null if the field key equals DESCRIPTION.
|
||||
*/
|
||||
private suspend fun mapToUiFieldsDraggableListItem(
|
||||
field: ObjectWrapper.Relation,
|
||||
stringResourceProvider: StringResourceProvider,
|
||||
storeOfObjectTypes: StoreOfObjectTypes,
|
||||
fieldParser: FieldParser,
|
||||
urlBuilder: UrlBuilder
|
||||
): UiFieldsListItem? {
|
||||
if (field.key == Relations.DESCRIPTION) return null
|
||||
|
||||
val limitObjectTypes = mapLimitObjectTypes(field, storeOfObjectTypes, fieldParser, urlBuilder)
|
||||
return Item.Draggable(
|
||||
id = field.id,
|
||||
fieldKey = field.key,
|
||||
fieldTitle = field.getName(stringResourceProvider),
|
||||
format = field.format,
|
||||
limitObjectTypes = limitObjectTypes,
|
||||
isEditableField = fieldParser.isFieldEditable(field),
|
||||
canDelete = fieldParser.isFieldCanBeDeletedFromType(field)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a field to a local UI list item.
|
||||
* Returns null if the field key equals DESCRIPTION.
|
||||
*/
|
||||
private suspend fun mapToUiFieldsLocalListItem(
|
||||
field: ObjectWrapper.Relation,
|
||||
stringResourceProvider: StringResourceProvider,
|
||||
storeOfObjectTypes: StoreOfObjectTypes,
|
||||
fieldParser: FieldParser,
|
||||
urlBuilder: UrlBuilder
|
||||
): UiFieldsListItem? {
|
||||
if (field.key == Relations.DESCRIPTION) return null
|
||||
|
||||
val limitObjectTypes = mapLimitObjectTypes(field, storeOfObjectTypes, fieldParser, urlBuilder)
|
||||
return Item.Local(
|
||||
id = field.id,
|
||||
fieldKey = field.key,
|
||||
fieldTitle = field.getName(stringResourceProvider),
|
||||
format = field.format,
|
||||
limitObjectTypes = limitObjectTypes,
|
||||
isEditableField = fieldParser.isFieldEditable(field)
|
||||
)
|
||||
}
|
||||
|
||||
fun ObjectWrapper.Relation.mapToUiAddFieldListItem(
|
||||
stringResourceProvider: StringResourceProvider
|
||||
): UiAddFieldItem? {
|
||||
val field = this
|
||||
if (field.key == Relations.DESCRIPTION) return null
|
||||
return UiAddFieldItem(
|
||||
id = field.id,
|
||||
fieldKey = field.key,
|
||||
fieldTitle = field.getName(stringResourceProvider),
|
||||
format = field.format
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.alerts
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
|
||||
import com.anytypeio.anytype.core_ui.foundation.BUTTON_PRIMARY
|
||||
import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY
|
||||
import com.anytypeio.anytype.core_ui.foundation.Dragger
|
||||
import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED
|
||||
import com.anytypeio.anytype.core_ui.foundation.GenericAlert
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeleteAlertScreen(
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
ModalBottomSheet(
|
||||
dragHandle = {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Dragger()
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
},
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
onTypeEvent(TypeEvent.OnAlertDeleteDismiss)
|
||||
},
|
||||
content = {
|
||||
GenericAlert(
|
||||
config = AlertConfig.WithTwoButtons(
|
||||
title = stringResource(R.string.are_you_sure_delete_one_object),
|
||||
description = stringResource(R.string.delete_irrevocably_one_object),
|
||||
firstButtonText = stringResource(R.string.cancel),
|
||||
secondButtonText = stringResource(R.string.delete),
|
||||
icon = AlertConfig.Icon(
|
||||
GRADIENT_TYPE_RED,
|
||||
icon = R.drawable.ic_alert_error
|
||||
),
|
||||
firstButtonType = BUTTON_SECONDARY,
|
||||
secondButtonType = BUTTON_PRIMARY,
|
||||
),
|
||||
onFirstButtonClicked = {
|
||||
onTypeEvent(TypeEvent.OnAlertDeleteDismiss)
|
||||
},
|
||||
onSecondButtonClicked = {
|
||||
onTypeEvent(TypeEvent.OnAlertDeleteConfirm)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun DeleteAlertScreenPreview() {
|
||||
DeleteAlertScreen(
|
||||
onTypeEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.header
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
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.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
|
||||
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
|
||||
|
||||
@Composable
|
||||
fun IconAndTitleWidget(
|
||||
modifier: Modifier,
|
||||
uiIconState: UiIconState,
|
||||
uiTitleState: UiTitleState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
ListWidgetObjectIcon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.noRippleThrottledClickable {
|
||||
if (uiIconState.isEditable) {
|
||||
onTypeEvent.invoke(TypeEvent.OnObjectTypeIconClick)
|
||||
}
|
||||
},
|
||||
icon = uiIconState.icon,
|
||||
backgroundColor = R.color.amp_transparent
|
||||
)
|
||||
NameField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
uiTitleState = uiTitleState,
|
||||
onTypeEvent = onTypeEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NameField(
|
||||
modifier: Modifier,
|
||||
uiTitleState: UiTitleState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
var innerValue by remember(uiTitleState.title) { mutableStateOf(uiTitleState.title) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
BasicTextField(
|
||||
value = innerValue,
|
||||
onValueChange = {
|
||||
innerValue = it
|
||||
onTypeEvent.invoke(
|
||||
TypeEvent.OnObjectTypeTitleUpdate(
|
||||
title = innerValue
|
||||
)
|
||||
)
|
||||
},
|
||||
textStyle = HeadlineTitle.copy(color = colorResource(id = R.color.text_primary)),
|
||||
singleLine = false,
|
||||
enabled = uiTitleState.isEditable,
|
||||
cursorBrush = SolidColor(colorResource(id = R.color.text_primary)),
|
||||
modifier = modifier
|
||||
.padding(start = 12.dp, end = 20.dp)
|
||||
.focusRequester(focusRequester),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onTypeEvent.invoke(
|
||||
TypeEvent.OnObjectTypeTitleUpdate(
|
||||
title = innerValue
|
||||
)
|
||||
)
|
||||
},
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(32.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (innerValue.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
text = stringResource(id = R.string.untitled),
|
||||
style = HeadlineTitle,
|
||||
color = colorResource(id = R.color.text_tertiary),
|
||||
)
|
||||
}
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun IconAndTitleWidgetPreview() {
|
||||
IconAndTitleWidget(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
onTypeEvent = {},
|
||||
uiIconState = UiIconState(icon = ObjectIcon.Task(isChecked = false), isEditable = true),
|
||||
uiTitleState = UiTitleState(
|
||||
title = "I understand that contributing to this repository will require me to agree with the",
|
||||
isEditable = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun IconAndTitleEmptyWidgetPreview() {
|
||||
IconAndTitleWidget(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
onTypeEvent = {},
|
||||
uiIconState = UiIconState(icon = ObjectIcon.Task(isChecked = false), isEditable = true),
|
||||
uiTitleState = UiTitleState(
|
||||
title = "",
|
||||
isEditable = true
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.header
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiFieldsButtonState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState
|
||||
|
||||
@Composable
|
||||
fun HorizontalButtons(
|
||||
modifier: Modifier,
|
||||
uiLayoutButtonState: UiLayoutButtonState,
|
||||
uiFieldsButtonState: UiFieldsButtonState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val modifierButton = Modifier
|
||||
.height(40.dp)
|
||||
.wrapContentWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(R.color.shape_primary),
|
||||
shape = RoundedCornerShape(size = 8.dp)
|
||||
)
|
||||
|
||||
if (uiLayoutButtonState is UiLayoutButtonState.Visible) {
|
||||
Row(
|
||||
modifier = modifierButton.noRippleThrottledClickable {
|
||||
onTypeEvent(TypeEvent.OnLayoutButtonClick)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(start = 12.dp),
|
||||
text = stringResource(R.string.button_layout),
|
||||
style = PreviewTitle2Medium,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(start = 6.dp, end = 12.dp),
|
||||
text = uiLayoutButtonState.layout.name.substring(0, 1).uppercase()
|
||||
+ uiLayoutButtonState.layout.name.substring(1)
|
||||
.toLowerCase(Locale.current),
|
||||
style = PreviewTitle2Medium,
|
||||
color = colorResource(R.color.glyph_active)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiFieldsButtonState is UiFieldsButtonState.Visible) {
|
||||
Row(
|
||||
modifier = modifierButton.noRippleThrottledClickable {
|
||||
onTypeEvent(TypeEvent.OnFieldsButtonClick)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(start = 12.dp),
|
||||
text = stringResource(R.string.button_fields),
|
||||
style = PreviewTitle2Medium,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(start = 6.dp, end = 12.dp),
|
||||
text = uiFieldsButtonState.count.toString(),
|
||||
style = PreviewTitle2Medium,
|
||||
color = colorResource(R.color.glyph_active)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun HorizontalButtonsPreview() {
|
||||
HorizontalButtons(
|
||||
modifier = Modifier
|
||||
.height(32.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp),
|
||||
uiLayoutButtonState = UiLayoutButtonState.Visible(ObjectType.Layout.BASIC),
|
||||
uiFieldsButtonState = UiFieldsButtonState.Visible(3),
|
||||
onTypeEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.header
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.syncstatus.StatusBadge
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiEditButton
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopToolbar(
|
||||
modifier: Modifier,
|
||||
uiEditButtonState: UiEditButton,
|
||||
uiSyncStatusBadgeState: UiSyncStatusBadgeState,
|
||||
topBarScrollBehavior: TopAppBarScrollBehavior,
|
||||
uiTitleState: UiTitleState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
val isIconMenuExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
expandedHeight = 48.dp,
|
||||
scrollBehavior = topBarScrollBehavior,
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
scrolledContainerColor = colorResource(id = R.color.background_primary),
|
||||
titleContentColor = colorResource(id = R.color.palette_system_red)
|
||||
),
|
||||
title = {
|
||||
if (topBarScrollBehavior.state.overlappedFraction > 0.7f) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = uiTitleState.title,
|
||||
style = PreviewTitle2Regular,
|
||||
color = colorResource(R.color.text_primary),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(56.dp)
|
||||
.height(48.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onTypeEvent(TypeEvent.OnBackClick)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
painter = painterResource(R.drawable.ic_default_top_back),
|
||||
contentDescription = stringResource(R.string.content_desc_back_button)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (uiSyncStatusBadgeState is UiSyncStatusBadgeState.Visible) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onTypeEvent(
|
||||
TypeEvent.OnSyncStatusClick(
|
||||
status = uiSyncStatusBadgeState.status
|
||||
)
|
||||
)
|
||||
},
|
||||
) {
|
||||
StatusBadge(
|
||||
status = uiSyncStatusBadgeState.status,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiEditButtonState is UiEditButton.Visible) {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.size(48.dp),
|
||||
onClick = {
|
||||
isIconMenuExpanded.value = !isIconMenuExpanded.value
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_space_list_dots),
|
||||
contentDescription = "More options"
|
||||
)
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.width(244.dp),
|
||||
expanded = isIconMenuExpanded.value,
|
||||
offset = DpOffset(x = 0.dp, y = 0.dp),
|
||||
onDismissRequest = {
|
||||
isIconMenuExpanded.value = false
|
||||
},
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_settings_item_remove),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.palette_system_red)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onTypeEvent(TypeEvent.OnMenuItemDeleteClick)
|
||||
isIconMenuExpanded.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun TopToolbarPreview() {
|
||||
val topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
|
||||
state = rememberTopAppBarState()
|
||||
)
|
||||
val spaceSyncUpdate = SpaceSyncUpdate.Update(
|
||||
id = "1",
|
||||
status = SpaceSyncStatus.SYNCING,
|
||||
network = SpaceSyncNetwork.ANYTYPE,
|
||||
error = SpaceSyncError.NULL,
|
||||
syncingObjectsCounter = 2
|
||||
)
|
||||
TopToolbar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible(
|
||||
status = SpaceSyncAndP2PStatusState.Success(
|
||||
spaceSyncUpdate = spaceSyncUpdate,
|
||||
p2PStatusUpdate = P2PStatusUpdate.Initial
|
||||
)
|
||||
),
|
||||
uiEditButtonState = UiEditButton.Visible,
|
||||
onTypeEvent = {},
|
||||
topBarScrollBehavior = topAppBarScrollBehavior,
|
||||
uiTitleState = UiTitleState(title = "Page", isEditable = true)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.layouts
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.Dragger
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.AvatarTitle
|
||||
import com.anytypeio.anytype.core_ui.views.Title1
|
||||
import com.anytypeio.anytype.core_ui.views.fontInterRegular
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TypeLayoutsScreen(
|
||||
modifier: Modifier,
|
||||
uiState: UiLayoutTypeState.Visible,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
dragHandle = {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Dragger()
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
},
|
||||
scrimColor = colorResource(id = R.color.modal_screen_outside_background),
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
sheetState = bottomSheetState,
|
||||
onDismissRequest = {
|
||||
onTypeEvent(TypeEvent.OnLayoutTypeDismiss)
|
||||
}
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.layout_type),
|
||||
style = Title1,
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(252.dp),
|
||||
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = 12.dp,
|
||||
alignment = Alignment.End
|
||||
),
|
||||
) {
|
||||
items(
|
||||
count = uiState.layouts.size,
|
||||
key = { index -> uiState.layouts[index].code },
|
||||
itemContent = {
|
||||
val item = uiState.layouts[it]
|
||||
val (borderWidth, borderColor) = if (item.code == uiState.selectedLayout?.code) {
|
||||
2.dp to colorResource(id = R.color.palette_system_amber_50)
|
||||
} else {
|
||||
1.dp to colorResource(id = R.color.shape_secondary)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TemplateItemContent(
|
||||
modifier = Modifier
|
||||
.width(120.dp)
|
||||
.height(224.dp)
|
||||
.border(
|
||||
width = borderWidth,
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp)
|
||||
.noRippleThrottledClickable{
|
||||
onTypeEvent(TypeEvent.OnLayoutTypeItemClick(item))
|
||||
onTypeEvent(TypeEvent.OnLayoutTypeDismiss)
|
||||
},
|
||||
item = item
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = item.name.substring(0, 1).uppercase()
|
||||
+ item.name.substring(1)
|
||||
.toLowerCase(Locale.current),
|
||||
style = TextStyle(
|
||||
fontFamily = fontInterRegular,
|
||||
fontWeight = FontWeight.W500,
|
||||
fontSize = 13.sp,
|
||||
letterSpacing = (-0.024).em
|
||||
),
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TemplateItemContent(
|
||||
modifier: Modifier,
|
||||
item: ObjectType.Layout
|
||||
) {
|
||||
when (item) {
|
||||
ObjectType.Layout.BASIC -> {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
shape = RoundedCornerShape(5.dp),
|
||||
color = colorResource(id = R.color.shape_tertiary)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
painter = painterResource(R.drawable.ic_type_layout_basic_icon),
|
||||
contentDescription = "Basic layout icon"
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.height(8.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(24.dp)
|
||||
.height(4.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BasicBlocks()
|
||||
}
|
||||
}
|
||||
|
||||
ObjectType.Layout.PROFILE -> {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
shape = CircleShape,
|
||||
color = colorResource(id = R.color.shape_tertiary)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
text = "N",
|
||||
style = AvatarTitle.copy(
|
||||
fontSize = 24.sp
|
||||
),
|
||||
color = colorResource(id = R.color.glyph_active),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.height(8.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(24.dp)
|
||||
.height(4.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BasicBlocks()
|
||||
}
|
||||
}
|
||||
|
||||
ObjectType.Layout.TODO -> {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Image(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
painter = painterResource(R.drawable.ic_type_layout_todo_icon),
|
||||
contentDescription = "Todo layout icon"
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.height(4.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BasicBlocks()
|
||||
}
|
||||
}
|
||||
|
||||
ObjectType.Layout.NOTE -> {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(46.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(24.dp)
|
||||
.height(4.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BasicBlocks()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.BasicBlocks() {
|
||||
repeat(3) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(88.dp)
|
||||
.height(6.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.height(6.dp)
|
||||
.background(
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 1.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun TypeLayoutsScreenPreview() {
|
||||
TypeLayoutsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
uiState = UiLayoutTypeState.Visible(
|
||||
layouts = listOf(
|
||||
ObjectType.Layout.PROFILE,
|
||||
ObjectType.Layout.BASIC,
|
||||
ObjectType.Layout.TODO,
|
||||
ObjectType.Layout.NOTE
|
||||
),
|
||||
selectedLayout = ObjectType.Layout.BASIC
|
||||
),
|
||||
onTypeEvent = {})
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.objects
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_models.DVSortType
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListMenuItem
|
||||
import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListSortingMenuContainer
|
||||
import com.anytypeio.anytype.core_ui.views.BodyBold
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiMenuSetItem
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiMenuState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiObjectsAddIconState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiObjectsHeaderState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiObjectsSettingsIconState
|
||||
import com.anytypeio.anytype.presentation.objects.MenuSortsItem
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectsListSort
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun ObjectsHeader(
|
||||
modifier: Modifier,
|
||||
uiObjectsHeaderState: UiObjectsHeaderState,
|
||||
uiObjectsAddIconState: UiObjectsAddIconState,
|
||||
uiObjectsSettingsIconState: UiObjectsSettingsIconState,
|
||||
uiObjectsMenuState: UiMenuState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
var isSortingExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.CenterVertically),
|
||||
text = stringResource(R.string.objects),
|
||||
style = BodyBold,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(start = 8.dp),
|
||||
text = uiObjectsHeaderState.count,
|
||||
style = PreviewTitle1Regular,
|
||||
color = colorResource(R.color.text_secondary)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
) {
|
||||
if (uiObjectsSettingsIconState is UiObjectsSettingsIconState.Visible) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(40.dp)
|
||||
.noRippleThrottledClickable {
|
||||
isMenuExpanded = !isMenuExpanded
|
||||
},
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
painter = painterResource(R.drawable.ic_space_list_dots),
|
||||
contentDescription = "Objects settings icon"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
modifier = Modifier.width(252.dp),
|
||||
expanded = isMenuExpanded,
|
||||
onDismissRequest = { isMenuExpanded = false },
|
||||
shape = RoundedCornerShape(size = 16.dp),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
shadowElevation = 5.dp
|
||||
) {
|
||||
when (val item = uiObjectsMenuState.objSetItem) {
|
||||
UiMenuSetItem.CreateSet -> {
|
||||
ObjectsListMenuItem(
|
||||
title = stringResource(R.string.object_type_objects_menu_create_set),
|
||||
isSelected = false,
|
||||
modifier = Modifier
|
||||
.clickable { onTypeEvent(TypeEvent.OnCreateSetClick) }
|
||||
)
|
||||
Divider(
|
||||
height = 8.dp,
|
||||
paddingStart = 0.dp,
|
||||
paddingEnd = 0.dp,
|
||||
color = colorResource(R.color.shape_secondary)
|
||||
)
|
||||
}
|
||||
|
||||
is UiMenuSetItem.OpenSet -> {
|
||||
ObjectsListMenuItem(
|
||||
title = stringResource(R.string.object_type_objects_menu_open_set),
|
||||
isSelected = false,
|
||||
modifier = Modifier
|
||||
.clickable { onTypeEvent(TypeEvent.OnOpenSetClick(setId = item.setId)) }
|
||||
)
|
||||
Divider(
|
||||
height = 8.dp,
|
||||
paddingStart = 0.dp,
|
||||
paddingEnd = 0.dp,
|
||||
color = colorResource(R.color.shape_secondary)
|
||||
)
|
||||
}
|
||||
|
||||
UiMenuSetItem.Hidden -> {}
|
||||
}
|
||||
ObjectsListSortingMenuContainer(
|
||||
container = uiObjectsMenuState.container,
|
||||
sorts = uiObjectsMenuState.sorts,
|
||||
types = uiObjectsMenuState.types,
|
||||
sortingExpanded = isSortingExpanded,
|
||||
onSortClick = {
|
||||
onTypeEvent(TypeEvent.OnSortClick(it))
|
||||
},
|
||||
onChangeSortExpandedState = { isSortingExpanded = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiObjectsAddIconState is UiObjectsAddIconState.Visible) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.height(48.dp)
|
||||
.width(32.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onTypeEvent(TypeEvent.OnCreateObjectIconClick)
|
||||
},
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
painter = painterResource(R.drawable.ic_default_plus),
|
||||
contentDescription = "Add",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun ObjectsHeaderPreview() {
|
||||
ObjectsHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
uiObjectsHeaderState = UiObjectsHeaderState("3"),
|
||||
uiObjectsAddIconState = UiObjectsAddIconState.Visible,
|
||||
uiObjectsSettingsIconState = UiObjectsSettingsIconState.Visible,
|
||||
uiObjectsMenuState = UiMenuState(
|
||||
container = MenuSortsItem.Container(
|
||||
sort = ObjectsListSort.ByName(isSelected = true)
|
||||
),
|
||||
sorts = listOf(
|
||||
MenuSortsItem.Sort(
|
||||
sort = ObjectsListSort.ByName(isSelected = true)
|
||||
),
|
||||
),
|
||||
types = listOf(
|
||||
MenuSortsItem.SortType(
|
||||
sort = ObjectsListSort.ByName(isSelected = true),
|
||||
sortType = DVSortType.DESC,
|
||||
isSelected = true
|
||||
),
|
||||
MenuSortsItem.SortType(
|
||||
sort = ObjectsListSort.ByDateCreated(isSelected = false),
|
||||
sortType = DVSortType.ASC,
|
||||
isSelected = false
|
||||
),
|
||||
),
|
||||
objSetItem = UiMenuSetItem.CreateSet
|
||||
),
|
||||
onTypeEvent = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.templates
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
import com.anytypeio.anytype.core_ui.views.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.widgets.TemplateItemContent
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TemplatesList(
|
||||
uiTemplatesListState: UiTemplatesListState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
|
||||
Timber.d("TemplatesList :$uiTemplatesListState")
|
||||
|
||||
val scrollState = rememberLazyListState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
LazyRow(
|
||||
state = scrollState,
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
items(
|
||||
count = uiTemplatesListState.items.size,
|
||||
key = { index ->
|
||||
val item = uiTemplatesListState.items[index]
|
||||
when (item) {
|
||||
is TemplateView.Blank -> item.id
|
||||
is TemplateView.New -> "new"
|
||||
is TemplateView.Template -> item.id
|
||||
}
|
||||
},
|
||||
itemContent = {
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
val item = uiTemplatesListState.items[it]
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(size = 16.dp)
|
||||
)
|
||||
.height(224.dp)
|
||||
.width(120.dp)
|
||||
.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(bounded = false, radius = 24.dp),
|
||||
onClick = {
|
||||
onTypeEvent(TypeEvent.OnTemplateItemClick(item))
|
||||
},
|
||||
onLongClick = {
|
||||
if (item is TemplateView.Template) {
|
||||
isMenuExpanded = true
|
||||
}
|
||||
},
|
||||
enabled = true,
|
||||
)
|
||||
) {
|
||||
TemplateItemContent(
|
||||
item = item,
|
||||
showDefaultIcon = true
|
||||
)
|
||||
DropdownMenu(
|
||||
modifier = Modifier.width(244.dp),
|
||||
expanded = isMenuExpanded,
|
||||
onDismissRequest = { isMenuExpanded = false },
|
||||
shape = RoundedCornerShape(size = 10.dp),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
shadowElevation = 5.dp,
|
||||
offset = DpOffset(
|
||||
x = 20.dp,
|
||||
y = (-300).dp
|
||||
)
|
||||
) {
|
||||
if (!item.isDefault) {
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.height(44.dp),
|
||||
onClick = {
|
||||
onTypeEvent(TypeEvent.OnTemplateMenuClick.SetAsDefault(item))
|
||||
isMenuExpanded = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_templates_menu_set_default),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
Divider(
|
||||
height = 0.5.dp,
|
||||
paddingStart = 0.dp,
|
||||
paddingEnd = 0.dp,
|
||||
color = colorResource(R.color.shape_primary)
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.height(44.dp),
|
||||
onClick = {
|
||||
onTypeEvent(TypeEvent.OnTemplateMenuClick.Edit(item))
|
||||
isMenuExpanded = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_templates_menu_edit),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
Divider(
|
||||
height = 0.5.dp,
|
||||
paddingStart = 0.dp,
|
||||
paddingEnd = 0.dp,
|
||||
color = colorResource(R.color.shape_primary)
|
||||
)
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.height(44.dp),
|
||||
onClick = {
|
||||
onTypeEvent(TypeEvent.OnTemplateMenuClick.Duplicate(item))
|
||||
isMenuExpanded = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_templates_menu_duplicate),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
Divider(
|
||||
height = 0.5.dp,
|
||||
paddingStart = 0.dp,
|
||||
paddingEnd = 0.dp,
|
||||
color = colorResource(R.color.shape_primary)
|
||||
)
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.height(44.dp),
|
||||
onClick = {
|
||||
onTypeEvent(TypeEvent.OnTemplateMenuClick.Delete(item))
|
||||
isMenuExpanded = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.object_type_templates_menu_delete),
|
||||
style = BodyRegular,
|
||||
color = colorResource(id = R.color.palette_system_red),
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.templates
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
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.ContentScale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
|
||||
import com.anytypeio.anytype.core_ui.views.BodyBold
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
|
||||
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent.OnTemplatesAddIconClick
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesAddIconState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesHeaderState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState
|
||||
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.TemplatesScreen(
|
||||
uiTemplatesHeaderState: UiTemplatesHeaderState.Visible,
|
||||
uiTemplatesAddIconState: UiTemplatesAddIconState,
|
||||
uiTemplatesListState: UiTemplatesListState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier.height(44.dp)
|
||||
)
|
||||
TemplatesHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
uiTemplatesHeaderState = uiTemplatesHeaderState,
|
||||
uiTemplatesAddIconState = uiTemplatesAddIconState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.height(12.dp)
|
||||
)
|
||||
TemplatesList(
|
||||
uiTemplatesListState = uiTemplatesListState,
|
||||
onTypeEvent = onTypeEvent
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.height(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TemplatesHeader(
|
||||
modifier: Modifier,
|
||||
uiTemplatesHeaderState: UiTemplatesHeaderState.Visible,
|
||||
uiTemplatesAddIconState: UiTemplatesAddIconState,
|
||||
onTypeEvent: (TypeEvent) -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(horizontal = 20.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.wrapContentWidth().align(Alignment.CenterStart)) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.CenterVertically),
|
||||
text = stringResource(R.string.templates),
|
||||
style = BodyBold,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(start = 8.dp),
|
||||
text = uiTemplatesHeaderState.count,
|
||||
style = PreviewTitle1Regular,
|
||||
color = colorResource(R.color.text_secondary)
|
||||
)
|
||||
}
|
||||
if (uiTemplatesAddIconState is UiTemplatesAddIconState.Visible) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.noRippleThrottledClickable {
|
||||
onTypeEvent(OnTemplatesAddIconClick)
|
||||
},
|
||||
painter = painterResource(R.drawable.ic_default_plus),
|
||||
contentDescription = "Add",
|
||||
contentScale = ContentScale.Inside
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun TemplatesHeaderPreview() {
|
||||
TemplatesHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
uiTemplatesHeaderState = UiTemplatesHeaderState.Visible(
|
||||
count = "2"
|
||||
),
|
||||
uiTemplatesAddIconState = UiTemplatesAddIconState.Visible,
|
||||
) { }
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,126 @@
|
|||
package com.anytypeio.anytype.feature_object_type.viewmodel
|
||||
|
||||
import com.anytypeio.anytype.core_models.DVFilter
|
||||
import com.anytypeio.anytype.core_models.DVFilterCondition
|
||||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.DVSortType
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
|
||||
fun filtersForSearch(
|
||||
objectTypeId: Id
|
||||
): List<DVFilter> {
|
||||
val filters = buildList {
|
||||
addAll(buildDeletedFilter())
|
||||
add(buildTemplateFilter())
|
||||
add(buildTypeIdFilter(listOf(objectTypeId)))
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
fun filtersForSetsSearch(
|
||||
objectTypeId: Id
|
||||
): List<DVFilter> {
|
||||
val filters = buildList {
|
||||
addAll(buildDeletedFilter())
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.LAYOUT,
|
||||
condition = DVFilterCondition.IN,
|
||||
value = listOf(ObjectType.Layout.SET.code.toDouble())
|
||||
)
|
||||
)
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.SET_OF,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = objectTypeId
|
||||
)
|
||||
)
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
fun filtersForTemplatesSearch(
|
||||
objectTypeId: Id
|
||||
): List<DVFilter> {
|
||||
val filters = buildList {
|
||||
addAll(buildDeletedFilter())
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.TYPE_UNIQUE_KEY,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = ObjectTypeUniqueKeys.TEMPLATE
|
||||
)
|
||||
)
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.TARGET_OBJECT_TYPE,
|
||||
condition = DVFilterCondition.EQUAL,
|
||||
value = objectTypeId
|
||||
)
|
||||
)
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
fun sortForSetSearch() = DVSort(
|
||||
relationKey = Relations.CREATED_DATE,
|
||||
type = DVSortType.DESC,
|
||||
includeTime = true,
|
||||
relationFormat = RelationFormat.DATE
|
||||
)
|
||||
|
||||
fun sortForTemplatesSearch() = DVSort(
|
||||
relationKey = Relations.LAST_MODIFIED_DATE,
|
||||
type = DVSortType.DESC,
|
||||
includeTime = true,
|
||||
relationFormat = RelationFormat.DATE
|
||||
)
|
||||
|
||||
private fun buildTypeIdFilter(ids: List<Id>): DVFilter = DVFilter(
|
||||
relation = Relations.TYPE,
|
||||
condition = DVFilterCondition.IN,
|
||||
value = ids
|
||||
)
|
||||
|
||||
|
||||
private fun buildDeletedFilter(): List<DVFilter> {
|
||||
return listOf(
|
||||
DVFilter(
|
||||
relation = Relations.IS_ARCHIVED,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
),
|
||||
DVFilter(
|
||||
relation = Relations.IS_HIDDEN,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
),
|
||||
DVFilter(
|
||||
relation = Relations.IS_DELETED,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
),
|
||||
DVFilter(
|
||||
relation = Relations.IS_HIDDEN_DISCOVERY,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildSpaceIdFilter(spaces: List<Id>): DVFilter = DVFilter(
|
||||
relation = Relations.SPACE_ID,
|
||||
condition = DVFilterCondition.IN,
|
||||
value = spaces
|
||||
)
|
||||
|
||||
private fun buildTemplateFilter(): DVFilter = DVFilter(
|
||||
relation = Relations.TYPE_UNIQUE_KEY,
|
||||
condition = DVFilterCondition.NOT_EQUAL,
|
||||
value = ObjectTypeUniqueKeys.TEMPLATE
|
||||
)
|
|
@ -0,0 +1,77 @@
|
|||
package com.anytypeio.anytype.feature_object_type.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
|
||||
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
|
||||
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
|
||||
import com.anytypeio.anytype.domain.`object`.DuplicateObjects
|
||||
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
|
||||
import com.anytypeio.anytype.domain.objects.DeleteObjects
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.page.CreateObject
|
||||
import com.anytypeio.anytype.domain.primitives.FieldParser
|
||||
import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields
|
||||
import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields
|
||||
import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import com.anytypeio.anytype.domain.templates.CreateTemplate
|
||||
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
|
||||
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
|
||||
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class ObjectTypeVMFactory @Inject constructor(
|
||||
private val vmParams: ObjectTypeVmParams,
|
||||
private val analytics: Analytics,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
|
||||
private val userPermissionProvider: UserPermissionProvider,
|
||||
private val storeOfRelations: StoreOfRelations,
|
||||
private val storeOfObjectTypes: StoreOfObjectTypes,
|
||||
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
|
||||
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
|
||||
private val createObject: CreateObject,
|
||||
private val fieldParser: FieldParser,
|
||||
private val coverImageHashProvider: CoverImageHashProvider,
|
||||
private val deleteObjects: DeleteObjects,
|
||||
private val setObjectDetails: SetObjectDetails,
|
||||
private val createObjectSet: CreateObjectSet,
|
||||
private val stringResourceProvider: StringResourceProvider,
|
||||
private val createTemplate: CreateTemplate,
|
||||
private val duplicateObjects: DuplicateObjects,
|
||||
private val getObjectTypeConflictingFields: GetObjectTypeConflictingFields,
|
||||
private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields,
|
||||
private val objectTypeSetHeaderRecommendedFields: SetObjectTypeHeaderRecommendedFields
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
ObjectTypeViewModel(
|
||||
vmParams = vmParams,
|
||||
analytics = analytics,
|
||||
urlBuilder = urlBuilder,
|
||||
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
|
||||
userPermissionProvider = userPermissionProvider,
|
||||
storeOfRelations = storeOfRelations,
|
||||
storeOfObjectTypes = storeOfObjectTypes,
|
||||
storelessSubscriptionContainer = storelessSubscriptionContainer,
|
||||
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
|
||||
createObject = createObject,
|
||||
fieldParser = fieldParser,
|
||||
coverImageHashProvider = coverImageHashProvider,
|
||||
deleteObjects = deleteObjects,
|
||||
setObjectDetails = setObjectDetails,
|
||||
createObjectSet = createObjectSet,
|
||||
stringResourceProvider = stringResourceProvider,
|
||||
createTemplate = createTemplate,
|
||||
duplicateObjects = duplicateObjects,
|
||||
getObjectTypeConflictingFields = getObjectTypeConflictingFields,
|
||||
objectTypeSetRecommendedFields = objectTypeSetRecommendedFields,
|
||||
objectTypeSetHeaderRecommendedFields = objectTypeSetHeaderRecommendedFields
|
||||
) as T
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
package com.anytypeio.anytype.feature_object_type
|
||||
|
||||
import android.util.Log
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.StubObjectType
|
||||
import com.anytypeio.anytype.core_models.StubRelationObject
|
||||
import com.anytypeio.anytype.domain.config.Gateway
|
||||
import com.anytypeio.anytype.domain.debugging.Logger
|
||||
import com.anytypeio.anytype.domain.misc.DateProvider
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes
|
||||
import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations
|
||||
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
|
||||
import com.anytypeio.anytype.domain.objects.StoreOfRelations
|
||||
import com.anytypeio.anytype.domain.primitives.FieldParser
|
||||
import com.anytypeio.anytype.domain.primitives.FieldParserImpl
|
||||
import com.anytypeio.anytype.domain.resources.StringResourceProvider
|
||||
import com.anytypeio.anytype.test_utils.MockDataFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.lachlanmckee.timberjunit.TimberTestRule
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class TestFieldsMappping {
|
||||
|
||||
@get:Rule
|
||||
val timberTestRule: TimberTestRule = TimberTestRule.builder()
|
||||
.minPriority(Log.DEBUG)
|
||||
.showThread(true)
|
||||
.showTimestamp(false)
|
||||
.onlyLogWhenTestFails(true)
|
||||
.build()
|
||||
|
||||
val space = MockDataFactory.randomUuid()
|
||||
|
||||
val field1 = StubRelationObject(
|
||||
id = "field1_id",
|
||||
key = "field1_key",
|
||||
name = "Field 1, text",
|
||||
format = RelationFormat.LONG_TEXT,
|
||||
spaceId = space
|
||||
)
|
||||
|
||||
val field2 = StubRelationObject(
|
||||
id = "field2_id",
|
||||
key = "field2_key",
|
||||
name = "Field 2, number",
|
||||
format = RelationFormat.NUMBER,
|
||||
spaceId = space
|
||||
)
|
||||
|
||||
val field3 = StubRelationObject(
|
||||
id = "field3_id",
|
||||
key = "field3_key",
|
||||
name = "Field 3, date",
|
||||
format = RelationFormat.DATE,
|
||||
spaceId = space
|
||||
)
|
||||
|
||||
val field4 = StubRelationObject(
|
||||
id = "field4_id",
|
||||
key = "field4_key",
|
||||
name = "Field 4, checkbox",
|
||||
format = RelationFormat.CHECKBOX,
|
||||
spaceId = space
|
||||
)
|
||||
|
||||
val field5 = StubRelationObject(
|
||||
id = "field5_id",
|
||||
key = "field5_key",
|
||||
name = "Field 5, Status",
|
||||
format = RelationFormat.STATUS,
|
||||
spaceId = space
|
||||
)
|
||||
|
||||
val fieldCreatedDate = StubRelationObject(
|
||||
id = "bafyreihas6lc5knc67lbeohaxjgfjzi3oazs2yvh7gbotcktefjynjqndq",
|
||||
key = Relations.CREATED_DATE,
|
||||
name = "Field Creation date",
|
||||
format = RelationFormat.DATE,
|
||||
isHidden = false,
|
||||
isReadOnly = true,
|
||||
isReadOnlyValue = true,
|
||||
spaceId = space,
|
||||
sourceObject = "_brcreatedDate"
|
||||
)
|
||||
|
||||
val fieldAssigneeObjType1 = StubObjectType()
|
||||
val fieldAssigneeObjType2 = StubObjectType()
|
||||
|
||||
val fieldAssignee = StubRelationObject(
|
||||
id = "bafyreibrqycr2w5q2db76f5l6hxfljwrgkrpqbulks6ppxsfu4hq5lwmue",
|
||||
key = "assignee",
|
||||
name = "Field Assignee",
|
||||
format = RelationFormat.OBJECT,
|
||||
objectTypes = listOf(
|
||||
fieldAssigneeObjType1.id,
|
||||
fieldAssigneeObjType2.id
|
||||
),
|
||||
isHidden = false,
|
||||
isReadOnly = false,
|
||||
isReadOnlyValue = false,
|
||||
spaceId = space,
|
||||
sourceObject = "_brassignee"
|
||||
)
|
||||
|
||||
val allSpaceRelations =
|
||||
listOf(field1, field2, field3, field4, field5, fieldCreatedDate, fieldAssignee)
|
||||
|
||||
val featuredFields = listOf(field1, field2, field5).map { it.id }
|
||||
|
||||
val sidebarFields = listOf(field3, field4, field5).map { it.id }
|
||||
|
||||
val hiddenFields = listOf(field5).map { it.id }
|
||||
|
||||
val testObjectType = StubObjectType(
|
||||
id = "test_object_type_id",
|
||||
uniqueKey = "test_object_type_unique_key",
|
||||
name = "Test custom object type",
|
||||
recommendedRelations = sidebarFields,
|
||||
recommendedFeaturedRelations = featuredFields,
|
||||
recommendedHiddenRelations = hiddenFields,
|
||||
layout = ObjectType.Layout.OBJECT_TYPE.code.toDouble(),
|
||||
recommendedLayout = ObjectType.Layout.TODO.code.toDouble(),
|
||||
space = space
|
||||
)
|
||||
|
||||
@Mock
|
||||
lateinit var stringResourceProvider: StringResourceProvider
|
||||
|
||||
@Mock
|
||||
lateinit var dateProvider: DateProvider
|
||||
|
||||
@Mock
|
||||
lateinit var logger: Logger
|
||||
|
||||
@Mock
|
||||
lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
|
||||
|
||||
@Mock
|
||||
lateinit var gateway: Gateway
|
||||
|
||||
lateinit var storeOfRelations: StoreOfRelations
|
||||
|
||||
lateinit var storeOfObjectTypes: StoreOfObjectTypes
|
||||
|
||||
lateinit var fieldParser: FieldParser
|
||||
|
||||
private lateinit var urlBuilder: UrlBuilder
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
storeOfRelations = DefaultStoreOfRelations()
|
||||
storeOfObjectTypes = DefaultStoreOfObjectTypes()
|
||||
urlBuilder = UrlBuilder(gateway)
|
||||
fieldParser =
|
||||
FieldParserImpl(
|
||||
dateProvider = dateProvider,
|
||||
logger = logger,
|
||||
getDateObjectByTimestamp = getDateObjectByTimestamp,
|
||||
stringResourceProvider = stringResourceProvider
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not filter featured fields by hidden`() = runTest {
|
||||
|
||||
storeOfRelations.apply {
|
||||
merge(allSpaceRelations)
|
||||
}
|
||||
|
||||
storeOfObjectTypes.apply {
|
||||
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
|
||||
}
|
||||
|
||||
val parsedFields = fieldParser.getObjectTypeParsedFields(
|
||||
objectType = testObjectType,
|
||||
storeOfRelations = storeOfRelations,
|
||||
objectTypeConflictingFieldsIds = listOf()
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(field1, field2, field5),
|
||||
actual = parsedFields.header
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not filter sidebar fields by hidden`() = runTest {
|
||||
|
||||
storeOfRelations.apply {
|
||||
merge(allSpaceRelations)
|
||||
}
|
||||
|
||||
storeOfObjectTypes.apply {
|
||||
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
|
||||
}
|
||||
|
||||
val parsedFields = fieldParser.getObjectTypeParsedFields(
|
||||
objectType = testObjectType,
|
||||
storeOfRelations = storeOfRelations,
|
||||
objectTypeConflictingFieldsIds = listOf()
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(field3, field4, field5),
|
||||
actual = parsedFields.sidebar
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should map hidden fields`() = runTest {
|
||||
|
||||
storeOfRelations.apply {
|
||||
merge(allSpaceRelations)
|
||||
}
|
||||
|
||||
storeOfObjectTypes.apply {
|
||||
merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1))
|
||||
}
|
||||
|
||||
val parsedFields = fieldParser.getObjectTypeParsedFields(
|
||||
objectType = testObjectType,
|
||||
storeOfRelations = storeOfRelations,
|
||||
objectTypeConflictingFieldsIds = listOf()
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = listOf(field5),
|
||||
actual = parsedFields.hidden
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue