From 8d7b63ab96a5b3a4b6c68d9eb5cbd12238c31bb9 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Wed, 7 May 2025 18:56:29 +0200 Subject: [PATCH] Add alteration to the GMItem interface. --- .../composeResources/values/strings.xml | 1 + .../ui/screen/gamemaster/common/tag/GMTag.kt | 11 +- .../item/edit/GMAlterationFieldItemUio.kt | 222 ++++++++++++++++++ .../gamemaster/item/edit/GMItemEditFactory.kt | 100 +++++++- .../gamemaster/item/edit/GMItemEditPage.kt | 81 +++++-- .../item/edit/GMItemEditViewModel.kt | 88 ++++++- .../PaddingValues+calculatePaddings.kt | 21 ++ 7 files changed, 487 insertions(+), 37 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMAlterationFieldItemUio.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 2af9d3a..2027681 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -318,6 +318,7 @@ Empilable Équipable Consommable + Ajouter une alteration Édition de personnage \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt index ac47243..c195595 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt @@ -12,6 +12,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape @@ -30,8 +31,9 @@ data class GMTagUio( val meta: Boolean, ) -@Stable +@Immutable object GmTagDefault { + @Stable val padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) } @@ -83,11 +85,16 @@ fun GMTag( } } +@Immutable +object GmTagButtonDefault { + @Stable + val padding = PaddingValues() +} @Composable fun GMTagButton( modifier: Modifier = Modifier, - padding: PaddingValues = GmTagDefault.padding, + padding: PaddingValues = GmTagButtonDefault.padding, shape: Shape = MaterialTheme.shapes.small, tag: GMTagUio, onTag: (() -> Unit)? = null, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMAlterationFieldItemUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMAlterationFieldItemUio.kt new file mode 100644 index 0000000..ade2211 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMAlterationFieldItemUio.kt @@ -0,0 +1,222 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExposedDropdownMenuBox +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +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.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.utils.extention.calculateHorizontalPaddings +import com.pixelized.desktop.lwa.utils.extention.ribbon +import kotlinx.coroutines.flow.StateFlow + +@Stable +data class GMAlterationFieldItemUio( + val key: String, + val value: StateFlow, + val error: StateFlow, + val menu: List, +) { + @Stable + data class Field( + val id: String, + val label: String, + ) +} + +@Immutable +data object AlterationFieldItemDefault { + @Stable + val padding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + + @Stable + val spacing = 8.dp +} + +@Composable +fun GMAlterationFieldItem( + modifier: Modifier = Modifier, + padding: PaddingValues = AlterationFieldItemDefault.padding, + spacing: Dp = AlterationFieldItemDefault.spacing, + field: GMAlterationFieldItemUio, + onAlteration: (GMAlterationFieldItemUio.Field?) -> Unit, + onRemoveField: (String) -> Unit, +) { + val error = field.error.collectAsState() + val menu = remember { mutableStateOf(false) } + + AlterationFieldContent( + modifier = modifier, + padding = padding, + spacing = spacing, + isOpen = menu, + isError = error, + field = field, + onOpenStateChange = { menu.value = it }, + onAlteration = { + menu.value = false + onAlteration(it) + }, + onRemoveField = onRemoveField, + ) +} + +@Composable +@OptIn(ExperimentalMaterialApi::class) +fun AlterationFieldContent( + modifier: Modifier = Modifier, + padding: PaddingValues, + spacing: Dp, + isOpen: State, + isError: State, + field: GMAlterationFieldItemUio, + onOpenStateChange: (Boolean) -> Unit, + onAlteration: (GMAlterationFieldItemUio.Field?) -> Unit, + onRemoveField: (String) -> Unit, +) { + val horizontal = padding.calculateHorizontalPaddings() + + Column( + modifier = Modifier + .animateContentSize() + .then(other = modifier), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = spacing), + ) { + ExposedDropdownMenuBox( + modifier = Modifier + .weight(weight = 1f) + .clip( + shape = MaterialTheme.shapes.small, + ) + .background( + color = MaterialTheme.lwa.colorScheme.elevated.base1dp, + ) + .ribbon( + color = when (isError.value) { + null -> Color.Transparent + else -> MaterialTheme.lwa.colorScheme.base.error + } + ), + expanded = isOpen.value, + onExpandedChange = onOpenStateChange, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(height = 48.dp) + .padding(paddingValues = horizontal), + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedContent( + modifier = Modifier.weight(1f), + targetState = field.value.value?.label, + transitionSpec = { + val enter = fadeIn() + val exit = fadeOut() + enter togetherWith exit using SizeTransform(clip = false) + }, + ) { + Text( + style = MaterialTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = it ?: "", + ) + } + val rotation = animateFloatAsState( + targetValue = if (isOpen.value) -180f else 0f, + ) + Icon( + modifier = Modifier.graphicsLayer { rotationZ = rotation.value }, + imageVector = Icons.Default.ArrowDropDown, + contentDescription = null + ) + } + ExposedDropdownMenu( + modifier = Modifier.heightIn(max = 480.dp), + expanded = isOpen.value, + onDismissRequest = { onOpenStateChange(false) } + ) { + field.menu.forEach { + DropdownMenuItem( + onClick = { onAlteration(it) }, + enabled = true, + interactionSource = remember { MutableInteractionSource() }, + content = { + Text( + color = MaterialTheme.colors.primary, + text = it.label, + ) + }, + ) + } + } + } + + IconButton( + modifier = Modifier + .size(size = 48.dp) + .background( + color = MaterialTheme.lwa.colorScheme.elevated.base1dp, + shape = MaterialTheme.shapes.small, + ), + onClick = { onRemoveField(field.key) }, + ) { + Icon( + imageVector = Icons.Default.Close, + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + } + } + isError.value?.let { + Text( + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.error, + text = it + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt index cb08b20..0aeeb8f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt @@ -4,9 +4,16 @@ import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextField import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory +import com.pixelized.shared.lwa.model.alteration.Alteration import com.pixelized.shared.lwa.model.item.Item import com.pixelized.shared.lwa.model.tag.Tag +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_description import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_id @@ -14,6 +21,7 @@ import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_ import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_label import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_thumbnail import org.jetbrains.compose.resources.getString +import java.util.UUID class GMItemEditFactory( private val tagFactory: GMTagFactory, @@ -21,6 +29,7 @@ class GMItemEditFactory( suspend fun createForm( item: Item?, tags: Collection, + alterationFlow: StateFlow>, ): GMItemEditViewModel.GMItemEditForm { val idFlow = createLwaTextFieldFlow( label = getString(Res.string.game_master__item__edit_id), @@ -51,6 +60,17 @@ class GMItemEditFactory( selectedTagIds = item?.tags ?: emptyList(), ) ) + val alterations = MutableStateFlow( + item?.alterations + ?.associate { alterationId -> + val field = GMItemEditViewModel.GMItemEditForm.GMAlterationFieldItemFlow( + errorFlow = MutableStateFlow(null), + valueFlow = MutableStateFlow(alterationId) + ) + field.key to field + } + ?: emptyMap() + ) return GMItemEditViewModel.GMItemEditForm( idFlow = idFlow, labelFlow = labelFlow, @@ -61,12 +81,15 @@ class GMItemEditFactory( equipableFlow = equipableFlow, consumableFlow = consumableFlow, tagFlow = tagFlow, + alterationsFlow = alterations, ) } - fun createForm( - form: GMItemEditViewModel.GMItemEditForm, + fun createPage( + scope: CoroutineScope, originId: String?, + alterations: Map, + form: GMItemEditViewModel.GMItemEditForm, ): GMItemEditPageUio { return GMItemEditPageUio( id = form.idFlow.createLwaTextField(enable = originId == null), @@ -86,31 +109,82 @@ class GMItemEditFactory( checked = form.consumableFlow, onCheckedChange = { form.consumableFlow.value = it }, ), + alterations = form.alterationsFlow.map { data -> + data.map { entry -> + createAlterationUio( + key = entry.key, + value = entry.value.valueFlow.mapNotNull { alterationId -> + if (alterationId == null) return@mapNotNull null + val alteration = alterations[alterationId] ?: return@mapNotNull null + GMAlterationFieldItemUio.Field( + id = alterationId, + label = alteration.metadata.name, + ) + }.stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = null, + ), + error = entry.value.errorFlow, + alterations = alterations.values, + ) + } + }.stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyList(), + ), tags = form.tagFlow, ) } fun createItem( - form: GMItemEditPageUio?, + form: GMItemEditViewModel.GMItemEditForm?, ): Item? { if (form == null) return null return Item( - id = form.id.valueFlow.value, + id = form.idFlow.valueFlow.value, metadata = Item.MetaData( - label = form.label.valueFlow.value, - description = form.description.valueFlow.value, - image = form.image.valueFlow.value, - thumbnail = form.thumbnail.valueFlow.value, + label = form.labelFlow.valueFlow.value, + description = form.descriptionFlow.valueFlow.value, + image = form.imageFlow.valueFlow.value, + thumbnail = form.thumbnailFlow.valueFlow.value, ), options = Item.Options( - stackable = form.stackable.checked.value, - equipable = form.equipable.checked.value, - consumable = form.consumable.checked.value, + stackable = form.stackableFlow.value, + equipable = form.equipableFlow.value, + consumable = form.consumableFlow.value, ), - tags = form.tags.value + tags = form.tagFlow.value .filter { it.highlight } .map { it.id }, - alterations = emptyList(), // TODO, + alterations = form.alterationsFlow.value.values + .mapNotNull { it.valueFlow.value }, + ) + } + + fun createAlterationUio( + key: String, + value: StateFlow, + error: StateFlow, + alterations: Collection, + ): GMAlterationFieldItemUio { + return GMAlterationFieldItemUio( + key = key, + value = value, + error = error, + menu = alterations.map { alteration -> + createAlterationFieldUio(alteration) + }, + ) + } + + private fun createAlterationFieldUio( + alteration: Alteration, + ): GMAlterationFieldItemUio.Field { + return GMAlterationFieldItemUio.Field( + id = alteration.id, + label = alteration.metadata.name, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt index fe6478e..0f21988 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt @@ -15,8 +15,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -37,11 +35,11 @@ import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,7 +47,7 @@ import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type -import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController @@ -65,15 +63,19 @@ import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagButton import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.utils.extention.calculateHorizontalPaddings +import com.pixelized.desktop.lwa.utils.extention.calculateVerticalPaddings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.game_master__action__save import lwacharactersheet.composeapp.generated.resources.game_master__item__create +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_add_alteration +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_consumable import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_equipable import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_stackable -import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_consumable import lwacharactersheet.composeapp.generated.resources.ic_save_24dp import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -89,6 +91,7 @@ data class GMItemEditPageUio( val stackable: LwaCheckBoxUio, val equipable: LwaCheckBoxUio, val consumable: LwaCheckBoxUio, + val alterations: StateFlow>, val tags: MutableStateFlow>, ) @@ -102,6 +105,7 @@ fun GMItemEditPage( viewModel: GMItemEditViewModel = koinViewModel(), ) { val screen = LocalScreenController.current + val focus = LocalFocusManager.current val scope = rememberCoroutineScope() val form = viewModel.form.collectAsState() @@ -110,16 +114,34 @@ fun GMItemEditPage( modifier = Modifier.fillMaxSize(), form = form, onBack = { + focus.clearFocus(force = true) screen.navigateBack() }, onSave = { + focus.clearFocus(force = true) scope.launch { if (viewModel.save()) { screen.navigateBack() } } }, + onAddAlteration = { + focus.clearFocus(force = true) + viewModel.addAlteration() + }, + onAlteration = { field, selected -> + focus.clearFocus(force = true) + viewModel.toggleAlteration( + field = field, + selected = selected, + ) + }, + onRemoveAlteration = { + focus.clearFocus(force = true) + viewModel.removeAlteration(key = it) + }, onTag = { tag -> + focus.clearFocus(force = true) viewModel.addTag(tag = tag) }, ) @@ -144,21 +166,13 @@ private fun GMItemEditContent( paddings: PaddingValues = GMItemEditDefault.paddings, onBack: () -> Unit, onSave: () -> Unit, + onAddAlteration: () -> Unit, + onAlteration: (GMAlterationFieldItemUio, GMAlterationFieldItemUio.Field?) -> Unit, + onRemoveAlteration: (String) -> Unit, onTag: (GMTagUio) -> Unit, ) { - val layoutDirection = LocalLayoutDirection.current - val verticalPadding = remember(paddings) { - PaddingValues( - top = paddings.calculateTopPadding(), - bottom = paddings.calculateBottomPadding(), - ) - } - val horizontalPadding = remember(paddings, layoutDirection) { - PaddingValues( - start = paddings.calculateStartPadding(layoutDirection = layoutDirection), - end = paddings.calculateEndPadding(layoutDirection = layoutDirection), - ) - } + val verticalPadding = paddings.calculateVerticalPaddings() + val horizontalPadding = paddings.calculateHorizontalPaddings() Scaffold( modifier = modifier, @@ -216,6 +230,7 @@ private fun GMItemEditContent( else -> { val tags = it.tags.collectAsState() + val alterations = it.alterations.collectAsState() LazyColumn( modifier = Modifier.fillMaxSize(), @@ -351,6 +366,22 @@ private fun GMItemEditContent( } } } + items( + items = alterations.value, + key = { field -> field.key }, + ) { field -> + GMAlterationFieldItem( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + field = field, + onAlteration = { alterationId -> + onAlteration(field, alterationId) + }, + onRemoveField = onRemoveAlteration, + ) + } item(key = "Actions") { Column( modifier = Modifier @@ -359,6 +390,20 @@ private fun GMItemEditContent( .padding(paddingValues = horizontalPadding), horizontalAlignment = Alignment.End ) { + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onAddAlteration, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__item__edit_add_alteration), + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + } Button( colors = LwaButtonColors(), shape = CircleShape, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt index e2ba1a3..f4621e1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.network.LwaNetworkException +import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.item.ItemRepository import com.pixelized.desktop.lwa.repository.tag.TagRepository import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio @@ -20,9 +21,11 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.UUID class GMItemEditViewModel( private val itemRepository: ItemRepository, + private val alterationRepository: AlterationRepository, private val tagRepository: TagRepository, private val factory: GMItemEditFactory, savedStateHandle: SavedStateHandle, @@ -35,9 +38,11 @@ class GMItemEditViewModel( private val _form = MutableStateFlow(null) val form: StateFlow = _form.map { if (it == null) return@map null - factory.createForm( - form = it, + factory.createPage( + scope = viewModelScope, originId = argument.id, + form = it, + alterations = alterationRepository.alterationFlow.value ) }.stateIn( scope = viewModelScope, @@ -50,12 +55,15 @@ class GMItemEditViewModel( _form.value = factory.createForm( item = itemRepository.item(itemId = argument.id), tags = tagRepository.itemsTags(), + alterationFlow = alterationRepository.alterationFlow, ) } } suspend fun save(): Boolean { - val edited = factory.createItem(form = form.value) ?: return false + if (!isFormValid()) return false + + val edited = factory.createItem(form = _form.value) ?: return false try { itemRepository.updateItem( @@ -77,6 +85,29 @@ class GMItemEditViewModel( } } + private suspend fun isFormValid(): Boolean { + var isValid = true + + // check for empty values + _form.value?.alterationsFlow?.value?.values + ?.forEach { field -> + if (field.valueFlow.value.isNullOrBlank()) { + field.errorFlow.value = "Empty alteration are not allowed." + isValid = false + } + } + + // check for duplicate alterations id. + _form.value?.alterationsFlow?.value?.values + ?.duplicates { it.valueFlow.value } + ?.forEach { field -> + field.errorFlow.value = "Duplicate alteration are not allowed." + isValid = false + } + + return isValid + } + fun addTag(tag: GMTagUio) { _form.value?.tagFlow?.update { tags -> tags.toMutableList().also { @@ -88,6 +119,38 @@ class GMItemEditViewModel( } } + fun toggleAlteration( + field: GMAlterationFieldItemUio, + selected: GMAlterationFieldItemUio.Field?, + ) { + _form.value?.alterationsFlow?.update { alterations -> + alterations.toMutableMap().also { + it[field.key]?.valueFlow?.value = selected?.id + it[field.key]?.errorFlow?.value = null + } + } + } + + fun addAlteration() { + _form.value?.alterationsFlow?.update { alterations -> + alterations.toMutableMap().also { + val flow = GMItemEditForm.GMAlterationFieldItemFlow( + errorFlow = MutableStateFlow(null), + valueFlow = MutableStateFlow(null), + ) + it[flow.key] = flow + } + } + } + + fun removeAlteration(key: String) { + _form.value?.alterationsFlow?.update { alterations -> + alterations.toMutableMap().also { + it.remove(key) + } + } + } + data class GMItemEditForm( val idFlow: LwaTextFieldFlow, val labelFlow: LwaTextFieldFlow, @@ -97,6 +160,23 @@ class GMItemEditViewModel( val stackableFlow: MutableStateFlow, val equipableFlow: MutableStateFlow, val consumableFlow: MutableStateFlow, + val alterationsFlow: MutableStateFlow>, val tagFlow: MutableStateFlow>, - ) + ) { + data class GMAlterationFieldItemFlow( + val key : String = "${UUID.randomUUID()}-${System.currentTimeMillis()}", + val errorFlow: MutableStateFlow, + val valueFlow: MutableStateFlow, + ) + } + + private inline fun Iterable.duplicates(selector: (T) -> R): List { + val set = HashSet() + val list = ArrayList() + for (e in this) { + val key = selector(e) + if (!set.add(key)) list.add(e) + } + return list + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/PaddingValues+calculatePaddings.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/PaddingValues+calculatePaddings.kt index 09078f4..722a711 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/PaddingValues+calculatePaddings.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/PaddingValues+calculatePaddings.kt @@ -24,6 +24,27 @@ fun PaddingValues.calculatePaddings( ) } +@Composable +@ReadOnlyComposable +fun PaddingValues.calculateVerticalPaddings(): PaddingValues { + return PaddingValues( + top = calculateTopPadding(), + bottom = calculateBottomPadding(), + ) +} + + +@Composable +@ReadOnlyComposable +fun PaddingValues.calculateHorizontalPaddings( + layoutDirection: LayoutDirection = LocalLayoutDirection.current, +): PaddingValues { + return PaddingValues( + start = calculateStartPadding(layoutDirection = layoutDirection), + end = calculateEndPadding(layoutDirection = layoutDirection), + ) +} + @Immutable @Stable data class ComputedPaddingValues(