anytype-kotlin-wild/app/src/main/java/com/anytypeio/anytype/ui/objects/creation/SelectObjectTypeScreen.kt

524 lines
20 KiB
Kotlin

package com.anytypeio.anytype.ui.objects.creation
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
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.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalFocusManager
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.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.extensions.throttledClick
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.EmptyState
import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.emojifier.Emojifier
import com.anytypeio.anytype.presentation.objects.SelectTypeView
import com.anytypeio.anytype.presentation.objects.SelectTypeViewState
import kotlinx.coroutines.delay
@Preview
@Composable
fun PreviewScreen() {
SelectObjectTypeScreen(
onTypeClicked = {},
state = SelectTypeViewState.Loading,
onQueryChanged = {},
onFocused = {},
onUnpinTypeClicked = {},
onPinOnTopClicked = {}
)
}
@Composable
fun SelectObjectTypeScreen(
onTypeClicked: (SelectTypeView.Type) -> Unit,
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
onPinOnTopClicked: (SelectTypeView.Type) -> Unit,
onQueryChanged: (String) -> Unit,
onFocused: () -> Unit,
state: SelectTypeViewState
) {
Column(
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
) {
Dragger(
Modifier
.align(Alignment.CenterHorizontally)
.padding(vertical = 6.dp)
.verticalScroll(rememberScrollState())
)
SearchField(
onQueryChanged = onQueryChanged,
onFocused = onFocused
)
Spacer(modifier = Modifier.height(8.dp))
ScreenContent(
state = state,
onTypeClicked = onTypeClicked,
onPinOnTopClicked = onPinOnTopClicked,
onUnpinTypeClicked = onUnpinTypeClicked
)
}
}
@Composable
private fun ScreenContent(
state: SelectTypeViewState,
onTypeClicked: (SelectTypeView.Type) -> Unit,
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
) {
when (state) {
is SelectTypeViewState.Content -> {
FlowRowContent(
views = state.views,
onTypeClicked = onTypeClicked,
onPinOnTopClicked = onPinOnTopClicked,
onUnpinTypeClicked = onUnpinTypeClicked
)
}
SelectTypeViewState.Empty -> {
AnimatedVisibility(
visible = true,
enter = fadeIn(animationSpec = tween(500)),
exit = fadeOut(animationSpec = tween(500))
) {
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
EmptyState(
modifier = Modifier.align(Alignment.Center),
title = stringResource(id = R.string.nothing_found),
description = stringResource(id = R.string.nothing_found_object_types),
icon = AlertConfig.Icon(
gradient = GRADIENT_TYPE_RED,
icon = R.drawable.ic_alert_error
)
)
}
}
}
SelectTypeViewState.Loading -> {}
}
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun FlowRowContent(
views: List<SelectTypeView>,
onTypeClicked: (SelectTypeView.Type) -> Unit,
onUnpinTypeClicked: (SelectTypeView.Type) -> Unit,
onPinOnTopClicked: (SelectTypeView.Type) -> Unit
) {
FlowRow(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
views.forEach { view ->
when (view) {
is SelectTypeView.Type -> {
val isMenuExpanded = remember {
mutableStateOf(false)
}
Box {
ObjectTypeItem(
name = view.name,
emoji = view.icon,
onItemClicked = throttledClick(
onClick = { onTypeClicked(view) }
),
onItemLongClicked = {
isMenuExpanded.value = !isMenuExpanded.value
},
modifier = Modifier
)
if (view.isPinnable) {
DropdownMenu(
expanded = isMenuExpanded.value,
onDismissRequest = { isMenuExpanded.value = false },
offset = DpOffset(x = 0.dp, y = 6.dp)
) {
if (!view.isPinned || !view.isFirstInSection) {
DropdownMenuItem(
onClick = {
isMenuExpanded.value = false
onPinOnTopClicked(view)
}
) {
Text(
text = stringResource(R.string.any_object_creation_menu_pin_on_top),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
if (view.isPinned) {
DropdownMenuItem(
onClick = {
isMenuExpanded.value = false
onUnpinTypeClicked(view)
}
) {
Text(
text = stringResource(R.string.any_object_creation_menu_unpin),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
}
}
}
}
is SelectTypeView.Section.Pinned -> {
Section(
title = stringResource(id = R.string.create_object_section_pinned),
)
}
is SelectTypeView.Section.Groups -> {
Section(
title = stringResource(id = R.string.create_object_section_lists),
)
}
is SelectTypeView.Section.Objects -> {
Section(
title = stringResource(id = R.string.create_object_section_objects)
)
}
is SelectTypeView.Section.Library -> {
Section(
title = stringResource(id = R.string.create_object_section_library),
)
}
}
}
}
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun LazyColumnContent(
views: List<SelectTypeView>,
onTypeClicked: (SelectTypeView.Type) -> Unit
) {
LazyVerticalGrid(
modifier = Modifier.fillMaxSize(),
columns = GridCells.Fixed(3),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(
start = 20.dp,
end = 20.dp
)
) {
views.forEach { view ->
when (view) {
is SelectTypeView.Section.Groups -> {
item(
key = view.javaClass.name,
span = { GridItemSpan(maxLineSpan) }
) {
Section(
title = stringResource(id = R.string.create_object_section_lists),
)
}
}
is SelectTypeView.Section.Objects -> {
item(
key = view.javaClass.name,
span = { GridItemSpan(maxLineSpan) }
) {
Section(
title = stringResource(id = R.string.create_object_section_objects)
)
}
}
is SelectTypeView.Section.Pinned -> {
item(
key = view.javaClass.name,
span = { GridItemSpan(maxLineSpan) }
) {
Section(
title = stringResource(id = R.string.create_object_section_pinned)
)
}
}
is SelectTypeView.Section.Library -> {
item(
key = view.javaClass.name,
span = { GridItemSpan(maxLineSpan) }
) {
Section(
title = stringResource(id = R.string.create_object_section_library)
)
}
}
is SelectTypeView.Type -> {
item(
key = view.typeKey
) {
ObjectTypeItem(
name = view.name,
emoji = view.icon,
onItemClicked = throttledClick(
onClick = {
onTypeClicked(view)
}
),
onItemLongClicked = {
},
modifier = Modifier.animateItemPlacement()
)
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ObjectTypeItem(
modifier: Modifier,
name: String,
emoji: String,
onItemClicked: () -> Unit,
onItemLongClicked: () -> Unit
) {
Row(
modifier = modifier
.height(48.dp)
.border(
width = 1.dp,
color = colorResource(id = R.color.shape_primary),
shape = RoundedCornerShape(12.dp)
)
.clip(RoundedCornerShape(12.dp))
.combinedClickable(
onClick = {
onItemClicked()
},
onLongClick = {
onItemLongClicked()
}
),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(
modifier = Modifier.width(14.dp)
)
val uri = Emojifier.safeUri(emoji)
if (uri.isNotEmpty()) {
Image(
painter = rememberAsyncImagePainter(
Emojifier.safeUri(emoji)
),
contentDescription = "Icon from URI",
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(
text = name,
style = Title2,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 16.dp)
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SearchField(
onQueryChanged: (String) -> Unit,
onFocused: () -> Unit
) {
Box(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
val focusManager = LocalFocusManager.current
val focusRequester = FocusRequester()
val input = remember { mutableStateOf(String()) }
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.height(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(color = colorResource(id = R.color.shape_transparent))
.align(Alignment.Center)
) {
Image(
painter = painterResource(id = R.drawable.ic_search_18),
contentDescription = "Search icon",
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 8.dp)
)
if (input.value.isNotEmpty()) {
Image(
painter = painterResource(id = R.drawable.ic_clear_18),
contentDescription = "Search icon",
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp)
.noRippleClickable {
input.value = ""
onQueryChanged("")
}
)
}
BasicTextField(
value = input.value,
onValueChange = {
input.value = it
onQueryChanged(it)
},
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() }
),
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp)
.align(Alignment.CenterStart)
.focusRequester(focusRequester)
.onFocusChanged { state ->
if (state.isFocused)
onFocused()
},
maxLines = 1,
singleLine = true,
textStyle = BodyRegular.copy(
color = colorResource(id = R.color.text_primary)
),
cursorBrush = SolidColor(
colorResource(id = R.color.cursor_color)
),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = input.value,
innerTextField = innerTextField,
singleLine = true,
enabled = true,
placeholder = {
Text(
text = stringResource(R.string.search_hint),
style = BodyRegular
)
},
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = colorResource(id = R.color.text_primary),
backgroundColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
placeholderColor = colorResource(id = R.color.text_tertiary)
),
interactionSource = remember { MutableInteractionSource() },
visualTransformation = VisualTransformation.None,
contentPadding = PaddingValues(
start = 0.dp,
top = 0.dp,
end = 0.dp,
bottom = 0.dp
),
border = {},
)
}
)
}
}
}
@Composable
private fun Section(title: String) {
Box(
modifier = Modifier
.height(44.dp)
.fillMaxWidth()
) {
Text(
modifier = Modifier
.align(Alignment.BottomStart),
text = title,
color = colorResource(id = R.color.text_secondary),
style = Caption1Medium
)
}
}