Add alteration to the GMItem interface.
This commit is contained in:
		
							parent
							
								
									168ee27826
								
							
						
					
					
						commit
						8d7b63ab96
					
				
					 7 changed files with 487 additions and 37 deletions
				
			
		| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Field?>,
 | 
			
		||||
    val error: StateFlow<String?>,
 | 
			
		||||
    val menu: List<Field>,
 | 
			
		||||
) {
 | 
			
		||||
    @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<Boolean>,
 | 
			
		||||
    isError: State<String?>,
 | 
			
		||||
    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
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Tag>,
 | 
			
		||||
        alterationFlow: StateFlow<Map<String, Alteration>>,
 | 
			
		||||
    ): 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<String, Alteration>,
 | 
			
		||||
        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<GMAlterationFieldItemUio.Field?>,
 | 
			
		||||
        error: StateFlow<String?>,
 | 
			
		||||
        alterations: Collection<Alteration>,
 | 
			
		||||
    ): 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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<GMAlterationFieldItemUio>>,
 | 
			
		||||
    val tags: MutableStateFlow<List<GMTagUio>>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<GMItemEditForm?>(null)
 | 
			
		||||
    val form: StateFlow<GMItemEditPageUio?> = _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<Boolean>,
 | 
			
		||||
        val equipableFlow: MutableStateFlow<Boolean>,
 | 
			
		||||
        val consumableFlow: MutableStateFlow<Boolean>,
 | 
			
		||||
        val alterationsFlow: MutableStateFlow<Map<String, GMAlterationFieldItemFlow>>,
 | 
			
		||||
        val tagFlow: MutableStateFlow<List<GMTagUio>>,
 | 
			
		||||
    )
 | 
			
		||||
    ) {
 | 
			
		||||
        data class GMAlterationFieldItemFlow(
 | 
			
		||||
            val key : String = "${UUID.randomUUID()}-${System.currentTimeMillis()}",
 | 
			
		||||
            val errorFlow: MutableStateFlow<String?>,
 | 
			
		||||
            val valueFlow: MutableStateFlow<String?>,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inline fun <T, R> Iterable<T>.duplicates(selector: (T) -> R): List<T> {
 | 
			
		||||
        val set = HashSet<R>()
 | 
			
		||||
        val list = ArrayList<T>()
 | 
			
		||||
        for (e in this) {
 | 
			
		||||
            val key = selector(e)
 | 
			
		||||
            if (!set.add(key)) list.add(e)
 | 
			
		||||
        }
 | 
			
		||||
        return list
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue