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

DROID-3435 Primitives | Type page as Set, part 2 (#2136)

This commit is contained in:
Konstantin Ivanov 2025-03-07 13:10:51 +01:00 committed by GitHub
parent f179d64231
commit 405f316f4d
Signed by: github
GPG key ID: B5690EEEBB952194
3 changed files with 549 additions and 26 deletions

View file

@ -1,7 +1,32 @@
package com.anytypeio.anytype.ui.primitives
import android.os.Build
import android.view.View
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.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.fragment.compose.AndroidFragment
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.BottomSyncStatus
import com.anytypeio.anytype.feature_object_type.ui.TopBarContent
import com.anytypeio.anytype.feature_object_type.ui.TypeEvent
import com.anytypeio.anytype.feature_object_type.ui.UiDeleteAlertState
import com.anytypeio.anytype.feature_object_type.ui.UiEditButton
@ -13,7 +38,13 @@ import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesButtonState
import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesModalListState
import com.anytypeio.anytype.feature_object_type.ui.UiTitleState
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.layouts.TypeLayoutsScreen
import com.anytypeio.anytype.feature_object_type.ui.templates.TemplatesModalList
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -39,5 +70,125 @@ fun WithSetScreen(
objectId: String,
space: String,
) {
//next PR
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 ->
MainContentSet(
paddingValues = paddingValues,
uiIconState = uiIconState,
uiTitleState = uiTitleState,
uiFieldsButtonState = uiFieldsButtonState,
uiLayoutButtonState = uiLayoutButtonState,
uiTemplatesButtonState = uiTemplatesButtonState,
objectId = objectId,
space = space,
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
)
}
if (uiTemplatesModalListState is UiTemplatesModalListState.Visible) {
TemplatesModalList(
modifier = Modifier.fillMaxWidth(),
uiState = uiTemplatesModalListState,
onTypeEvent = onTypeEvent
)
}
}
@Composable
private fun MainContentSet(
paddingValues: PaddingValues,
uiIconState: UiIconState,
uiTitleState: UiTitleState,
uiFieldsButtonState: UiFieldsButtonState,
uiLayoutButtonState: UiLayoutButtonState,
uiTemplatesButtonState: UiTemplatesButtonState,
objectId: String,
space: String,
onTypeEvent: (TypeEvent) -> Unit
) {
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)
}
Column(modifier = contentModifier) {
IconAndTitleWidget(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 35.dp)
.padding(horizontal = 20.dp),
uiIconState = uiIconState,
uiTitleState = uiTitleState,
onTypeEvent = onTypeEvent
)
Spacer(modifier = Modifier.height(20.dp))
HorizontalButtons(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.padding(horizontal = 20.dp),
uiFieldsButtonState = uiFieldsButtonState,
uiLayoutButtonState = uiLayoutButtonState,
uiTemplatesButtonState = uiTemplatesButtonState,
onTypeEvent = onTypeEvent
)
Spacer(modifier = Modifier.height(24.dp))
AndroidFragment<ObjectSetFragment>(
modifier = Modifier
.fillMaxSize(),
arguments = ObjectSetFragment.args(
ctx = objectId,
space = space
)
) { fragment ->
fragment.view?.findViewById<View>(R.id.objectHeader)?.visibility =
View.GONE
}
}
}

View file

@ -1499,34 +1499,44 @@ open class ObjectSetFragment :
}
private fun observeSelectingTemplate() {
val navController = findNavController()
val navBackStackEntry = navController.getBackStackEntry(R.id.objectSetScreen)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(ARG_TEMPLATE_ID)) {
val resultTemplateId = navBackStackEntry.savedStateHandle.get<String>(ARG_TEMPLATE_ID)
val resultTypeId = navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_ID)
val resultTypeKey = navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_KEY)
if (!resultTemplateId.isNullOrBlank() && !resultTypeId.isNullOrBlank() && !resultTypeKey.isNullOrBlank()) {
navBackStackEntry.savedStateHandle.remove<String>(ARG_TEMPLATE_ID)
navBackStackEntry.savedStateHandle.remove<String>(ARG_TARGET_TYPE_ID)
navBackStackEntry.savedStateHandle.remove<String>(ARG_TARGET_TYPE_KEY)
vm.proceedWithSelectedTemplate(
template = resultTemplateId,
typeId = resultTypeId,
typeKey = resultTypeKey
)
try {
val navController = findNavController()
val navBackStackEntry = navController.getBackStackEntry(R.id.objectSetScreen)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(ARG_TEMPLATE_ID)
) {
val resultTemplateId =
navBackStackEntry.savedStateHandle.get<String>(ARG_TEMPLATE_ID)
val resultTypeId =
navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_ID)
val resultTypeKey =
navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_KEY)
if (!resultTemplateId.isNullOrBlank() && !resultTypeId.isNullOrBlank() && !resultTypeKey.isNullOrBlank()) {
navBackStackEntry.savedStateHandle.remove<String>(ARG_TEMPLATE_ID)
navBackStackEntry.savedStateHandle.remove<String>(ARG_TARGET_TYPE_ID)
navBackStackEntry.savedStateHandle.remove<String>(ARG_TARGET_TYPE_KEY)
vm.proceedWithSelectedTemplate(
template = resultTemplateId,
typeId = resultTypeId,
typeKey = resultTypeKey
)
}
}
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
} catch (
e: Exception
) {
Timber.w(e)
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
override fun injectDependencies() {

View file

@ -0,0 +1,362 @@
package com.anytypeio.anytype.feature_object_type.ui.templates
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
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.BoxScope
import androidx.compose.foundation.layout.Column
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.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ObjectType
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.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.widgets.TemplateItemContent
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.UiTemplatesModalListState
import com.anytypeio.anytype.presentation.editor.cover.CoverColor
import com.anytypeio.anytype.presentation.templates.TemplateView
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun TemplatesModalList(
modifier: Modifier,
uiState: UiTemplatesModalListState.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.OnTemplatesModalListDismiss)
}
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
Text(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.templates),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
if (uiState.showAddIcon) {
IconAdd(onTypeEvent = onTypeEvent)
}
}
Spacer(modifier = Modifier.height(8.dp))
if (uiState.items.isEmpty()) {
EmptyState(
modifier = Modifier
.height(224.dp)
.fillMaxWidth()
)
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
horizontalArrangement = Arrangement.spacedBy(
space = 12.dp,
alignment = Alignment.Start
),
) {
items(
count = uiState.items.size,
key = { index ->
val item = uiState.items[index]
when (item) {
is TemplateView.Blank -> item.id
is TemplateView.New -> "new"
is TemplateView.Template -> item.id
}
},
itemContent = { index ->
ItemContent(
modifier = Modifier,
item = uiState.items[index],
onTypeEvent = onTypeEvent
)
}
)
}
Spacer(modifier = Modifier.height(11.dp))
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun EmptyState(modifier: Modifier
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.object_type_templates_empty),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_secondary),
textAlign = TextAlign.Center,
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun LazyItemScope.ItemContent(
modifier: Modifier,
item: TemplateView,
onTypeEvent: (TypeEvent) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
var isMenuExpanded by remember { mutableStateOf(false) }
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,
) {
if (!item.isDefault) {
DropdownMenuItem(
modifier = Modifier.height(44.dp),
onClick = {
onTypeEvent(TypeEvent.OnTemplateMenuClick.SetAsDefault(item))
isMenuExpanded = false
}
) {
androidx.compose.material.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
)
}
}
}
}
@Composable
private fun BoxScope.IconAdd(onTypeEvent: (TypeEvent) -> Unit) {
Box(
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterEnd)
.noRippleThrottledClickable {
onTypeEvent(TypeEvent.OnTemplatesAddIconClick)
}
) {
Image(
painter = painterResource(id = R.drawable.ic_default_plus),
contentDescription = "Add new Template icon",
modifier = Modifier
.height(48.dp)
.width(56.dp)
.align(Alignment.Center),
contentScale = ContentScale.None
)
}
}
@DefaultPreviews
@Composable
fun TemplatesModalListPreview() {
TemplatesModalList(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
uiState = UiTemplatesModalListState.Visible(
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,
)
),
showAddIcon = true
),
onTypeEvent = {}
)
}
@DefaultPreviews
@Composable
fun TemplatesModalListEmptyPreview() {
TemplatesModalList(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
uiState = UiTemplatesModalListState.Visible(
items = listOf(),
showAddIcon = true
),
onTypeEvent = {}
)
}