mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-3367 Primitives | Object type icons, part 1 (#2156)
This commit is contained in:
parent
f952730bfa
commit
fc0386b4ef
46 changed files with 837 additions and 561 deletions
|
@ -2,6 +2,8 @@ package com.anytypeio.anytype.feature_object_type.ui
|
|||
|
||||
import com.anytypeio.anytype.core_models.ObjectType.Layout
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState
|
||||
import com.anytypeio.anytype.presentation.objects.ObjectIcon
|
||||
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
|
||||
import com.anytypeio.anytype.presentation.templates.TemplateView
|
||||
|
||||
sealed class TypeEvent {
|
||||
|
@ -42,4 +44,10 @@ sealed class TypeEvent {
|
|||
data object OnLayoutButtonClick : TypeEvent()
|
||||
data object OnFieldsButtonClick : TypeEvent()
|
||||
data object OnTemplatesButtonClick : TypeEvent()
|
||||
|
||||
//region Icon picker
|
||||
data class OnIconPickerItemClick(val iconName: String, val color: CustomIconColor?) : TypeEvent()
|
||||
data object OnIconPickerRemovedClick : TypeEvent()
|
||||
data object OnIconPickerDismiss : TypeEvent()
|
||||
//endregion
|
||||
}
|
|
@ -29,8 +29,6 @@ sealed class ObjectTypeCommand {
|
|||
val spaceId: Id
|
||||
) : ObjectTypeCommand()
|
||||
|
||||
data object OpenEmojiPicker : ObjectTypeCommand()
|
||||
|
||||
data object OpenFieldsScreen : ObjectTypeCommand()
|
||||
|
||||
data class OpenEditTypePropertiesScreen(val typeId: Id, val space: Id) : ObjectTypeCommand()
|
||||
|
@ -230,4 +228,12 @@ sealed class UiSyncStatusBadgeState {
|
|||
data object Hidden : UiSyncStatusBadgeState()
|
||||
data class Visible(val status: SpaceSyncAndP2PStatusState) : UiSyncStatusBadgeState()
|
||||
}
|
||||
//endregion
|
||||
//endregion
|
||||
|
||||
//region Type icon screen
|
||||
sealed class UiIconsPickerState {
|
||||
data object Hidden : UiIconsPickerState()
|
||||
data object Visible : UiIconsPickerState()
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
|
|
@ -0,0 +1,307 @@
|
|||
package com.anytypeio.anytype.feature_object_type.ui.icons
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.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.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.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.colorRes
|
||||
import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar
|
||||
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.BodyRegular
|
||||
import com.anytypeio.anytype.core_ui.views.Title1
|
||||
import com.anytypeio.anytype.core_ui.widgets.objectIcon.custom_icons.CustomIcons
|
||||
import com.anytypeio.anytype.feature_object_type.R
|
||||
import com.anytypeio.anytype.feature_object_type.ui.icons.ChangeIconScreenConst.secondRowColors
|
||||
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangeIconScreen(
|
||||
modifier: Modifier,
|
||||
onDismissRequest: () -> Unit,
|
||||
onIconClicked: (String, CustomIconColor?) -> Unit,
|
||||
onRemoveIconClicked: () -> Unit
|
||||
) {
|
||||
|
||||
val bottomSheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
val allIconNames = remember { CustomIcons.iconsMap.keys.toList() }
|
||||
|
||||
ModalBottomSheet(
|
||||
modifier = modifier.windowInsetsPadding(WindowInsets.statusBars),
|
||||
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 = onDismissRequest
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = stringResource(R.string.object_type_icon_change_title),
|
||||
style = Title1,
|
||||
color = colorResource(R.color.text_primary),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 16.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onRemoveIconClicked()
|
||||
},
|
||||
text = stringResource(R.string.object_type_icon_remove),
|
||||
style = BodyRegular,
|
||||
color = colorResource(R.color.palette_system_red),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
DefaultSearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
hint = R.string.object_type_icon_change_title_search_hint
|
||||
) { newQuery ->
|
||||
searchQuery = newQuery
|
||||
}
|
||||
|
||||
Divider(paddingStart = 0.dp, paddingEnd = 0.dp)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val filteredIcons = if (searchQuery.isEmpty()) {
|
||||
allIconNames
|
||||
} else {
|
||||
allIconNames.filter { it.contains(searchQuery, ignoreCase = true) }
|
||||
}
|
||||
|
||||
IconSelectionGrid(
|
||||
icons = filteredIcons,
|
||||
onIconClicked = onIconClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconSelectionGrid(
|
||||
modifier: Modifier = Modifier,
|
||||
icons: List<String>,
|
||||
onIconClicked: (String, CustomIconColor?) -> Unit
|
||||
) {
|
||||
|
||||
val hapticFeedback = rememberReorderHapticFeedback()
|
||||
|
||||
LazyVerticalGrid(
|
||||
modifier = modifier,
|
||||
columns = GridCells.Adaptive(minSize = 57.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(
|
||||
items = icons,
|
||||
key = { iconName -> iconName },
|
||||
contentType = { "icon" }
|
||||
) { iconName ->
|
||||
IconItem(
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
hapticFeedback = hapticFeedback,
|
||||
iconName = iconName,
|
||||
onIconClicked = onIconClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun IconItem(
|
||||
modifier: Modifier,
|
||||
iconName: String,
|
||||
hapticFeedback: ReorderHapticFeedback,
|
||||
onIconClicked: (String, CustomIconColor?) -> Unit
|
||||
) {
|
||||
val showIconPreviews = remember { mutableStateOf(false) }
|
||||
Box(modifier = modifier
|
||||
.combinedClickable(
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onIconClicked(iconName, null)
|
||||
},
|
||||
onLongClick = {
|
||||
hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.START)
|
||||
showIconPreviews.value = true
|
||||
}
|
||||
)) {
|
||||
val imageVector = CustomIcons.getImageVector(iconName)
|
||||
val tintColor = if (!showIconPreviews.value) {
|
||||
colorResource(id = CustomIconColor.Gray.colorRes())
|
||||
} else {
|
||||
colorResource(id = R.color.glyph_inactive)
|
||||
}
|
||||
if (imageVector != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.align(Alignment.Center),
|
||||
imageVector = imageVector,
|
||||
contentDescription = "Object Type icon",
|
||||
colorFilter = ColorFilter.tint(tintColor),
|
||||
)
|
||||
IconPreviews(
|
||||
imageVector = imageVector,
|
||||
show = showIconPreviews.value,
|
||||
onDismissRequest = { showIconPreviews.value = false },
|
||||
onIconClicked = { color ->
|
||||
showIconPreviews.value = false
|
||||
onIconClicked(iconName, color)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconPreviews(
|
||||
imageVector: ImageVector,
|
||||
show: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onIconClicked: (CustomIconColor) -> Unit
|
||||
) {
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
expanded = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
shape = RoundedCornerShape(size = 20.dp),
|
||||
containerColor = colorResource(id = R.color.background_primary),
|
||||
shadowElevation = 5.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ChangeIconScreenConst.firstRowColors.forEach { customColor ->
|
||||
val color = colorResource(id = customColor.colorRes())
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onIconClicked(customColor)
|
||||
},
|
||||
imageVector = imageVector,
|
||||
contentDescription = "Object Type icon",
|
||||
colorFilter = ColorFilter.tint(color),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
secondRowColors.forEach { customColor ->
|
||||
val color = colorResource(id = customColor.colorRes())
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.noRippleThrottledClickable {
|
||||
onIconClicked(customColor)
|
||||
},
|
||||
imageVector = imageVector,
|
||||
contentDescription = "Object Type icon",
|
||||
colorFilter = ColorFilter.tint(color),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ChangeIconScreenConst {
|
||||
val firstRowColors = listOf(
|
||||
CustomIconColor.Gray,
|
||||
CustomIconColor.Yellow,
|
||||
CustomIconColor.Amber,
|
||||
CustomIconColor.Red,
|
||||
CustomIconColor.Pink
|
||||
)
|
||||
|
||||
val secondRowColors = listOf(
|
||||
CustomIconColor.Purple,
|
||||
CustomIconColor.Blue,
|
||||
CustomIconColor.Sky,
|
||||
CustomIconColor.Teal,
|
||||
CustomIconColor.Green
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DefaultPreviews
|
||||
fun DefaultChangeIconScreenPreview() {
|
||||
IconSelectionGrid(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
icons = CustomIcons.iconsMap.keys.toList(),
|
||||
onIconClicked = { _, _ -> },
|
||||
)
|
||||
}
|
|
@ -32,13 +32,13 @@ import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem
|
|||
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.ObjectTypeCommand
|
||||
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand.OpenEmojiPicker
|
||||
import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams
|
||||
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
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiErrorState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiFieldsButtonState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiIconState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState
|
||||
import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState
|
||||
|
@ -55,6 +55,7 @@ import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
|
|||
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
|
||||
import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenObjectType
|
||||
import com.anytypeio.anytype.presentation.mapper.objectIcon
|
||||
import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor
|
||||
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
|
||||
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
|
||||
import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState
|
||||
|
@ -135,6 +136,9 @@ class ObjectTypeViewModel(
|
|||
//edit property
|
||||
val uiEditPropertyScreen = MutableStateFlow<UiEditPropertyState>(UiEditPropertyState.Hidden)
|
||||
|
||||
//icons picker screen
|
||||
val uiIconsPickerScreen = MutableStateFlow<UiIconsPickerState>(UiIconsPickerState.Hidden)
|
||||
|
||||
//error
|
||||
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
|
||||
//endregion
|
||||
|
@ -449,9 +453,7 @@ class ObjectTypeViewModel(
|
|||
}
|
||||
|
||||
TypeEvent.OnObjectTypeIconClick -> {
|
||||
viewModelScope.launch {
|
||||
commands.emit(OpenEmojiPicker)
|
||||
}
|
||||
uiIconsPickerScreen.value = UiIconsPickerState.Visible
|
||||
}
|
||||
|
||||
is TypeEvent.OnTemplateItemClick -> {
|
||||
|
@ -489,6 +491,20 @@ class ObjectTypeViewModel(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
TypeEvent.OnIconPickerDismiss -> {
|
||||
uiIconsPickerScreen.value = UiIconsPickerState.Hidden
|
||||
}
|
||||
|
||||
is TypeEvent.OnIconPickerItemClick -> {
|
||||
uiIconsPickerScreen.value = UiIconsPickerState.Hidden
|
||||
updateIcon(iconName = event.iconName, newColor = event.color)
|
||||
}
|
||||
|
||||
TypeEvent.OnIconPickerRemovedClick -> {
|
||||
uiIconsPickerScreen.value = UiIconsPickerState.Hidden
|
||||
removeIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -584,30 +600,39 @@ class ObjectTypeViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun updateIcon(
|
||||
emoji: String
|
||||
private fun updateIcon(
|
||||
iconName: String,
|
||||
newColor: CustomIconColor?
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val params = SetObjectDetails.Params(
|
||||
ctx = vmParams.objectId,
|
||||
details = mapOf(Relations.ICON_EMOJI to emoji)
|
||||
details = mapOf(
|
||||
Relations.ICON_EMOJI to null,
|
||||
Relations.ICON_NAME to iconName,
|
||||
Relations.ICON_OPTION to newColor?.iconOption?.toDouble()
|
||||
)
|
||||
)
|
||||
setObjectDetails.async(params).fold(
|
||||
onFailure = { error ->
|
||||
Timber.e(error, "Error while updating data view record")
|
||||
},
|
||||
onSuccess = {
|
||||
|
||||
Timber.d("Object type icon updated to icon: $iconName")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeIcon() {
|
||||
private fun removeIcon() {
|
||||
viewModelScope.launch {
|
||||
val params = SetObjectDetails.Params(
|
||||
ctx = vmParams.objectId,
|
||||
details = mapOf(Relations.ICON_EMOJI to null)
|
||||
details = mapOf(
|
||||
Relations.ICON_EMOJI to null,
|
||||
Relations.ICON_NAME to null,
|
||||
Relations.ICON_OPTION to null
|
||||
)
|
||||
)
|
||||
setObjectDetails.async(params).fold(
|
||||
onFailure = { error ->
|
||||
|
@ -700,6 +725,7 @@ class ObjectTypeViewModel(
|
|||
currentList.add(toIndex, item)
|
||||
uiFieldsListState.value = UiFieldsListState(items = currentList)
|
||||
}
|
||||
|
||||
is FieldEvent.EditProperty -> proceedWithEditPropertyEvent(event)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue