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

DROID-2121 Relations | Create or edit options screen (#821)

This commit is contained in:
Konstantin Ivanov 2024-02-05 16:11:46 +01:00 committed by GitHub
parent 4af8a939f9
commit ad58c376d3
Signed by: github
GPG key ID: B5690EEEBB952194
6 changed files with 430 additions and 105 deletions

View file

@ -12,9 +12,11 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_ui.relations.CreateOrEditOptionScreen
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.argStringOrNull
import com.anytypeio.anytype.core_utils.ext.subscribe
@ -54,12 +56,18 @@ class CreateOrEditOptionFragment : BaseBottomSheetComposeFragment() {
colors = MaterialTheme.colors.copy(
surface = colorResource(id = R.color.context_menu_background)
)
) {}
) {
CreateOrEditOptionScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
onButtonClicked = { vm.onButtonClick() },
onTextChanged = { vm.updateName(it) },
onColorChanged = { vm.updateColor(it) }
)
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
skipCollapsed()

View file

@ -12,11 +12,14 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_ui.relations.RelationsLazyList
import com.anytypeio.anytype.core_ui.relations.TagOrStatusValueScreen
import com.anytypeio.anytype.core_utils.ext.argBoolean
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.subscribe
@ -55,11 +58,11 @@ class TagOrStatusValueFragment : BaseBottomSheetComposeFragment() {
surface = colorResource(id = R.color.context_menu_background)
)
) {
// RelationsValueScreen(
// state = vm.viewState.collectAsStateWithLifecycle().value,
// action = vm::onAction,
// onQueryChanged = vm::onQueryChanged
// )
TagOrStatusValueScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
action = vm::onAction,
onQueryChanged = vm::onQueryChanged
)
}
}
}

View file

@ -0,0 +1,268 @@
package com.anytypeio.anytype.core_ui.relations
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
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.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.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.extensions.dark
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
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.Title1
import com.anytypeio.anytype.presentation.relations.option.CreateOrEditOptionScreenViewState
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CreateOrEditOptionScreen(
state: CreateOrEditOptionScreenViewState,
onButtonClicked: () -> Unit,
onTextChanged: (String) -> Unit,
onColorChanged: (ThemeColor) -> Unit
) {
val focusRequester = remember { FocusRequester() }
val currentState by rememberUpdatedState(state)
val keyboardController = LocalSoftwareKeyboardController.current
var selectedColor by remember { mutableStateOf(currentState.color) }
var editableText by remember { mutableStateOf(currentState.text) }
val (title, buttonText) = getTexts(currentState)
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.verticalScroll(rememberScrollState())
.padding(bottom = 20.dp, start = 20.dp, end = 20.dp)
) {
Header(text = title)
TextInput(
initialValue = currentState.text,
color = currentState.color,
onTextChanged = {
editableText = it
onTextChanged(it)
},
focusRequester = focusRequester,
keyboardController = keyboardController
)
Divider(paddingEnd = 0.dp, paddingStart = 0.dp)
Spacer(modifier = Modifier.height(26.dp))
Text(
text = stringResource(id = R.string.color),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
Spacer(modifier = Modifier.height(20.dp))
CirclesContainer(selectedColor = selectedColor) { newColor ->
selectedColor = newColor
onColorChanged(newColor)
}
Spacer(modifier = Modifier.height(115.dp))
ButtonPrimary(
text = buttonText,
modifier = Modifier
.fillMaxWidth()
.imePadding(),
onClick = { onButtonClicked() },
size = ButtonSize.Large
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CirclesContainer(selectedColor: ThemeColor, action: (ThemeColor) -> Unit) {
FlowRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = 5
) {
val itemModifier = Modifier
.weight(1f)
.fillMaxWidth()
.clip(CircleShape)
.aspectRatio(1f)
.align(Alignment.CenterVertically)
ThemeColor.values().drop(1).forEach { color ->
Box(
itemModifier
.noRippleClickable { action(color) }
.background(
color = dark(color = color)
)
) {
if (selectedColor == color) {
Image(
modifier = Modifier.align(Alignment.Center),
painter = painterResource(id = R.drawable.ic_tick_24),
contentDescription = "option selected"
)
}
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun TextInput(
initialValue: String,
color: ThemeColor,
focusRequester: FocusRequester,
keyboardController: SoftwareKeyboardController?,
onTextChanged: (String) -> Unit
) {
var innerValue by remember { mutableStateOf(initialValue) }
val focusManager = LocalFocusManager.current
if (initialValue.isEmpty()) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
BasicTextField(
value = innerValue,
onValueChange = {
innerValue = it
onTextChanged(it)
},
textStyle = Title1.copy(color = dark(color = color)),
singleLine = true,
enabled = true,
cursorBrush = SolidColor(dark(color = color)),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 0.dp, top = 2.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions {
keyboardController?.hide()
focusManager.clearFocus()
},
decorationBox = { innerTextField ->
if (innerValue.isEmpty()) {
Text(
text = stringResource(id = R.string.hint_enter_name),
style = Title1,
color = colorResource(id = R.color.text_tertiary),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
)
}
innerTextField()
}
)
}
@Composable
private fun Header(text: String) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
) {
Dragger(modifier = Modifier.align(Alignment.Center))
}
// Main content box
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 74.dp),
text = text,
style = Title1.copy(),
color = colorResource(R.color.text_primary),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
}
@Composable
private fun getTexts(state: CreateOrEditOptionScreenViewState): Pair<String, String> {
return when (state) {
is CreateOrEditOptionScreenViewState.Create -> {
stringResource(id = R.string.option_widget_create) to stringResource(id = R.string.create)
}
is CreateOrEditOptionScreenViewState.Edit -> {
stringResource(id = R.string.option_widget_edit) to stringResource(id = R.string.apply)
}
}
}
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, device = Devices.PIXEL_4_XL)
@Composable
fun PreviewOptionWidget() {
CreateOrEditOptionScreen(
state = CreateOrEditOptionScreenViewState.Create(
text = "Urgent",
color = ThemeColor.BLUE,
),
onButtonClicked = {},
onTextChanged = {},
onColorChanged = {}
)
}

View file

@ -1,63 +0,0 @@
package com.anytypeio.anytype.core_ui.relations
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.presentation.relations.option.OptionScreenViewState
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun OptionWidget(
state: OptionScreenViewState,
onButtonClicked: () -> Unit,
onTextChanged: (String) -> Unit,
onColorChanged: (ThemeColor) -> Unit
) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentAlignment = Alignment.TopCenter,
) {
val currentState by rememberUpdatedState(state)
val keyboardController = LocalSoftwareKeyboardController.current
OptionWidgetContent(
state = currentState,
onButtonClicked = onButtonClicked,
focusRequester = focusRequester,
keyboardController = keyboardController,
onTextChanged = onTextChanged,
onColorChanged = onColorChanged
)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun OptionWidgetContent(
state: OptionScreenViewState,
onButtonClicked: () -> Unit,
focusRequester: FocusRequester,
keyboardController: SoftwareKeyboardController?,
onTextChanged: (String) -> Unit,
onColorChanged: (ThemeColor) -> Unit
) {
}

View file

@ -26,9 +26,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
@ -42,7 +40,7 @@ import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAct
import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewState
@Composable
fun RelationsValueScreen(
fun TagOrStatusValueScreen(
state: TagStatusViewState,
action: (TagStatusAction) -> Unit,
onQueryChanged: (String) -> Unit
@ -193,12 +191,14 @@ fun RelationsViewLoading() {
private fun isClearButtonVisible(state: TagStatusViewState): Boolean {
if (state !is TagStatusViewState.Content) return false
return state.items.any { it is RelationsListItem.Item.Tag && it.isSelected
|| it is RelationsListItem.Item.Status && it.isSelected } && state.isRelationEditable
return state.items.any {
it is RelationsListItem.Item.Tag && it.isSelected
|| it is RelationsListItem.Item.Status && it.isSelected
} && state.isRelationEditable
}
private fun isPlusButtonVisible(state: TagStatusViewState): Boolean {
return when (state) {
return when (state) {
is TagStatusViewState.Content -> state.isRelationEditable
is TagStatusViewState.Empty -> state.isRelationEditable
is TagStatusViewState.Loading -> false
@ -211,29 +211,4 @@ private fun getTitle(state: TagStatusViewState): String {
is TagStatusViewState.Empty -> state.title
is TagStatusViewState.Loading -> ""
}
}
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
@Composable
fun MyWidgetHeader() {
Header(state = TagStatusViewState.Content(
isRelationEditable = true,
title = "Tags",
items = listOf(
RelationsListItem.Item.Tag(
name = "Urgent",
color = ThemeColor.RED,
number = 1,
isSelected = true,
optionId = "1"
),
RelationsListItem.Item.Tag(
name = "Personal",
color = ThemeColor.ORANGE,
number = 2,
isSelected = false,
optionId = "1"
)
)
), action = {})
}

View file

@ -1,18 +1,26 @@
package com.anytypeio.anytype.presentation.relations.option
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_utils.ext.typeOf
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.relations.CreateRelationOption
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class CreateOrEditOptionViewModel(
private val viewModelParams: ViewModelParams,
@ -25,7 +33,133 @@ class CreateOrEditOptionViewModel(
) : BaseViewModel() {
val command = MutableSharedFlow<Command>(replay = 0)
val viewState: MutableStateFlow<OptionScreenViewState?>? = null
val viewState: MutableStateFlow<CreateOrEditOptionScreenViewState> =
MutableStateFlow(initialViewState())
private fun initialViewState(): CreateOrEditOptionScreenViewState {
val optionId = viewModelParams.optionId
val color = getOptionColor()
return if (optionId != null) {
Timber.d("Editing option with id: $optionId")
CreateOrEditOptionScreenViewState.Edit(
optionId = optionId,
text = viewModelParams.name.orEmpty(),
color = color
)
} else {
Timber.d("Creating new option")
CreateOrEditOptionScreenViewState.Create(
text = viewModelParams.name.orEmpty(),
color = color
)
}
}
fun updateName(name: String) {
viewState.value = when (val state = viewState.value) {
is CreateOrEditOptionScreenViewState.Create -> state.copy(text = name)
is CreateOrEditOptionScreenViewState.Edit -> state.copy(text = name)
}
}
fun updateColor(color: ThemeColor) {
viewState.value = when (val state = viewState.value) {
is CreateOrEditOptionScreenViewState.Create -> state.copy(color = color)
is CreateOrEditOptionScreenViewState.Edit -> state.copy(color = color)
}
}
fun onButtonClick() {
when (viewState.value) {
is CreateOrEditOptionScreenViewState.Create -> proceedWithCreatingOption()
is CreateOrEditOptionScreenViewState.Edit -> proceedWithUpdatingOption()
}
}
private fun proceedWithCreatingOption() {
viewModelScope.launch {
val params = CreateRelationOption.Params(
space = spaceManager.get(),
relation = viewModelParams.relationKey,
name = viewState.value.text,
color = viewState.value.color.code
)
if (params.name.isNotEmpty()) {
createOption.invoke(params).proceed(
success = { option ->
proceedWithAddingTagToObject(
ctx = viewModelParams.ctx,
objectId = viewModelParams.objectId,
relationKey = viewModelParams.relationKey,
option = option
)
},
failure = { Timber.e(it, "Error while creating option") }
)
}
}
}
private fun proceedWithUpdatingOption() {
val optionId = viewModelParams.optionId ?: return
viewModelScope.launch {
val params = SetObjectDetails.Params(
ctx = optionId,
details = mapOf(
Relations.NAME to viewState.value.text,
Relations.RELATION_OPTION_COLOR to viewState.value.color.code
)
)
setObjectDetails.execute(params).fold(
onFailure = { Timber.e(it, "Error while updating option") },
onSuccess = {
dispatcher.send(it)
viewModelScope.sendAnalyticsRelationValueEvent(analytics)
command.emit(Command.Dismiss)
}
)
}
}
private suspend fun proceedWithAddingTagToObject(
ctx: Id,
objectId: Id,
relationKey: Key,
option: ObjectWrapper.Option
) {
Timber.d("Adding option to object with id: $objectId")
val obj = values.get(target = objectId, ctx = ctx)
val result = mutableListOf<Id>()
val value = obj[relationKey]
if (value is List<*>) {
result.addAll(value.typeOf())
} else if (value is Id) {
result.add(value)
}
result.add(option.id)
val params = SetObjectDetails.Params(
ctx = objectId,
details = mapOf(relationKey to result)
)
setObjectDetails.execute(params).fold(
onFailure = { Timber.e(it, "Error while adding tag to object") },
onSuccess = {
dispatcher.send(it)
viewModelScope.sendAnalyticsRelationValueEvent(analytics)
command.emit(Command.Dismiss)
}
)
}
private fun getOptionColor(): ThemeColor {
val color = viewModelParams.color
return if (color != null) {
ThemeColor.fromCode(color)
} else {
ThemeColor.values().filter { it != ThemeColor.DEFAULT }.random()
}
}
data class ViewModelParams(
val ctx: Id,
@ -41,7 +175,7 @@ class CreateOrEditOptionViewModel(
}
}
sealed class OptionScreenViewState {
sealed class CreateOrEditOptionScreenViewState {
abstract val text: String
abstract val color: ThemeColor
@ -49,10 +183,10 @@ sealed class OptionScreenViewState {
val optionId: Id,
override val text: String,
override val color: ThemeColor
) : OptionScreenViewState()
) : CreateOrEditOptionScreenViewState()
data class Create(
override val text: String,
override val color: ThemeColor
) : OptionScreenViewState()
) : CreateOrEditOptionScreenViewState()
}