Add alteration to the GMItem interface.
This commit is contained in:
parent
168ee27826
commit
8d7b63ab96
7 changed files with 487 additions and 37 deletions
|
|
@ -318,6 +318,7 @@
|
||||||
<string name="game_master__item__edit_stackable">Empilable</string>
|
<string name="game_master__item__edit_stackable">Empilable</string>
|
||||||
<string name="game_master__item__edit_equipable">Équipable</string>
|
<string name="game_master__item__edit_equipable">Équipable</string>
|
||||||
<string name="game_master__item__edit_consumable">Consommable</string>
|
<string name="game_master__item__edit_consumable">Consommable</string>
|
||||||
|
<string name="game_master__item__edit_add_alteration">Ajouter une alteration</string>
|
||||||
<string name="game_master__character_edit__title">Édition de personnage</string>
|
<string name="game_master__character_edit__title">Édition de personnage</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
|
@ -30,8 +31,9 @@ data class GMTagUio(
|
||||||
val meta: Boolean,
|
val meta: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Stable
|
@Immutable
|
||||||
object GmTagDefault {
|
object GmTagDefault {
|
||||||
|
@Stable
|
||||||
val padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
val padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,11 +85,16 @@ fun GMTag(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
object GmTagButtonDefault {
|
||||||
|
@Stable
|
||||||
|
val padding = PaddingValues()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GMTagButton(
|
fun GMTagButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
padding: PaddingValues = GmTagDefault.padding,
|
padding: PaddingValues = GmTagButtonDefault.padding,
|
||||||
shape: Shape = MaterialTheme.shapes.small,
|
shape: Shape = MaterialTheme.shapes.small,
|
||||||
tag: GMTagUio,
|
tag: GMTagUio,
|
||||||
onTag: (() -> Unit)? = null,
|
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.createLwaTextField
|
||||||
import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow
|
import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow
|
||||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory
|
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.item.Item
|
||||||
import com.pixelized.shared.lwa.model.tag.Tag
|
import com.pixelized.shared.lwa.model.tag.Tag
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
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.Res
|
||||||
import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_description
|
import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_description
|
||||||
import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_id
|
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_label
|
||||||
import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_thumbnail
|
import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_thumbnail
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class GMItemEditFactory(
|
class GMItemEditFactory(
|
||||||
private val tagFactory: GMTagFactory,
|
private val tagFactory: GMTagFactory,
|
||||||
|
|
@ -21,6 +29,7 @@ class GMItemEditFactory(
|
||||||
suspend fun createForm(
|
suspend fun createForm(
|
||||||
item: Item?,
|
item: Item?,
|
||||||
tags: Collection<Tag>,
|
tags: Collection<Tag>,
|
||||||
|
alterationFlow: StateFlow<Map<String, Alteration>>,
|
||||||
): GMItemEditViewModel.GMItemEditForm {
|
): GMItemEditViewModel.GMItemEditForm {
|
||||||
val idFlow = createLwaTextFieldFlow(
|
val idFlow = createLwaTextFieldFlow(
|
||||||
label = getString(Res.string.game_master__item__edit_id),
|
label = getString(Res.string.game_master__item__edit_id),
|
||||||
|
|
@ -51,6 +60,17 @@ class GMItemEditFactory(
|
||||||
selectedTagIds = item?.tags ?: emptyList(),
|
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(
|
return GMItemEditViewModel.GMItemEditForm(
|
||||||
idFlow = idFlow,
|
idFlow = idFlow,
|
||||||
labelFlow = labelFlow,
|
labelFlow = labelFlow,
|
||||||
|
|
@ -61,12 +81,15 @@ class GMItemEditFactory(
|
||||||
equipableFlow = equipableFlow,
|
equipableFlow = equipableFlow,
|
||||||
consumableFlow = consumableFlow,
|
consumableFlow = consumableFlow,
|
||||||
tagFlow = tagFlow,
|
tagFlow = tagFlow,
|
||||||
|
alterationsFlow = alterations,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createForm(
|
fun createPage(
|
||||||
form: GMItemEditViewModel.GMItemEditForm,
|
scope: CoroutineScope,
|
||||||
originId: String?,
|
originId: String?,
|
||||||
|
alterations: Map<String, Alteration>,
|
||||||
|
form: GMItemEditViewModel.GMItemEditForm,
|
||||||
): GMItemEditPageUio {
|
): GMItemEditPageUio {
|
||||||
return GMItemEditPageUio(
|
return GMItemEditPageUio(
|
||||||
id = form.idFlow.createLwaTextField(enable = originId == null),
|
id = form.idFlow.createLwaTextField(enable = originId == null),
|
||||||
|
|
@ -86,31 +109,82 @@ class GMItemEditFactory(
|
||||||
checked = form.consumableFlow,
|
checked = form.consumableFlow,
|
||||||
onCheckedChange = { form.consumableFlow.value = it },
|
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,
|
tags = form.tagFlow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createItem(
|
fun createItem(
|
||||||
form: GMItemEditPageUio?,
|
form: GMItemEditViewModel.GMItemEditForm?,
|
||||||
): Item? {
|
): Item? {
|
||||||
if (form == null) return null
|
if (form == null) return null
|
||||||
return Item(
|
return Item(
|
||||||
id = form.id.valueFlow.value,
|
id = form.idFlow.valueFlow.value,
|
||||||
metadata = Item.MetaData(
|
metadata = Item.MetaData(
|
||||||
label = form.label.valueFlow.value,
|
label = form.labelFlow.valueFlow.value,
|
||||||
description = form.description.valueFlow.value,
|
description = form.descriptionFlow.valueFlow.value,
|
||||||
image = form.image.valueFlow.value,
|
image = form.imageFlow.valueFlow.value,
|
||||||
thumbnail = form.thumbnail.valueFlow.value,
|
thumbnail = form.thumbnailFlow.valueFlow.value,
|
||||||
),
|
),
|
||||||
options = Item.Options(
|
options = Item.Options(
|
||||||
stackable = form.stackable.checked.value,
|
stackable = form.stackableFlow.value,
|
||||||
equipable = form.equipable.checked.value,
|
equipable = form.equipableFlow.value,
|
||||||
consumable = form.consumable.checked.value,
|
consumable = form.consumableFlow.value,
|
||||||
),
|
),
|
||||||
tags = form.tags.value
|
tags = form.tagFlow.value
|
||||||
.filter { it.highlight }
|
.filter { it.highlight }
|
||||||
.map { it.id },
|
.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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -37,11 +35,11 @@ import androidx.compose.material.TextButton
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.type
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
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.screen.gamemaster.common.tag.GMTagUio
|
||||||
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
|
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
|
||||||
import com.pixelized.desktop.lwa.ui.theme.lwa
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import lwacharactersheet.composeapp.generated.resources.Res
|
import lwacharactersheet.composeapp.generated.resources.Res
|
||||||
import lwacharactersheet.composeapp.generated.resources.game_master__action__save
|
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__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_equipable
|
||||||
import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_stackable
|
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 lwacharactersheet.composeapp.generated.resources.ic_save_24dp
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
@ -89,6 +91,7 @@ data class GMItemEditPageUio(
|
||||||
val stackable: LwaCheckBoxUio,
|
val stackable: LwaCheckBoxUio,
|
||||||
val equipable: LwaCheckBoxUio,
|
val equipable: LwaCheckBoxUio,
|
||||||
val consumable: LwaCheckBoxUio,
|
val consumable: LwaCheckBoxUio,
|
||||||
|
val alterations: StateFlow<List<GMAlterationFieldItemUio>>,
|
||||||
val tags: MutableStateFlow<List<GMTagUio>>,
|
val tags: MutableStateFlow<List<GMTagUio>>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -102,6 +105,7 @@ fun GMItemEditPage(
|
||||||
viewModel: GMItemEditViewModel = koinViewModel(),
|
viewModel: GMItemEditViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val screen = LocalScreenController.current
|
val screen = LocalScreenController.current
|
||||||
|
val focus = LocalFocusManager.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val form = viewModel.form.collectAsState()
|
val form = viewModel.form.collectAsState()
|
||||||
|
|
@ -110,16 +114,34 @@ fun GMItemEditPage(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
form = form,
|
form = form,
|
||||||
onBack = {
|
onBack = {
|
||||||
|
focus.clearFocus(force = true)
|
||||||
screen.navigateBack()
|
screen.navigateBack()
|
||||||
},
|
},
|
||||||
onSave = {
|
onSave = {
|
||||||
|
focus.clearFocus(force = true)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (viewModel.save()) {
|
if (viewModel.save()) {
|
||||||
screen.navigateBack()
|
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 ->
|
onTag = { tag ->
|
||||||
|
focus.clearFocus(force = true)
|
||||||
viewModel.addTag(tag = tag)
|
viewModel.addTag(tag = tag)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -144,21 +166,13 @@ private fun GMItemEditContent(
|
||||||
paddings: PaddingValues = GMItemEditDefault.paddings,
|
paddings: PaddingValues = GMItemEditDefault.paddings,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
|
onAddAlteration: () -> Unit,
|
||||||
|
onAlteration: (GMAlterationFieldItemUio, GMAlterationFieldItemUio.Field?) -> Unit,
|
||||||
|
onRemoveAlteration: (String) -> Unit,
|
||||||
onTag: (GMTagUio) -> Unit,
|
onTag: (GMTagUio) -> Unit,
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val verticalPadding = paddings.calculateVerticalPaddings()
|
||||||
val verticalPadding = remember(paddings) {
|
val horizontalPadding = paddings.calculateHorizontalPaddings()
|
||||||
PaddingValues(
|
|
||||||
top = paddings.calculateTopPadding(),
|
|
||||||
bottom = paddings.calculateBottomPadding(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val horizontalPadding = remember(paddings, layoutDirection) {
|
|
||||||
PaddingValues(
|
|
||||||
start = paddings.calculateStartPadding(layoutDirection = layoutDirection),
|
|
||||||
end = paddings.calculateEndPadding(layoutDirection = layoutDirection),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -216,6 +230,7 @@ private fun GMItemEditContent(
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val tags = it.tags.collectAsState()
|
val tags = it.tags.collectAsState()
|
||||||
|
val alterations = it.alterations.collectAsState()
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
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") {
|
item(key = "Actions") {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -359,6 +390,20 @@ private fun GMItemEditContent(
|
||||||
.padding(paddingValues = horizontalPadding),
|
.padding(paddingValues = horizontalPadding),
|
||||||
horizontalAlignment = Alignment.End
|
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(
|
Button(
|
||||||
colors = LwaButtonColors(),
|
colors = LwaButtonColors(),
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.pixelized.desktop.lwa.network.LwaNetworkException
|
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.item.ItemRepository
|
||||||
import com.pixelized.desktop.lwa.repository.tag.TagRepository
|
import com.pixelized.desktop.lwa.repository.tag.TagRepository
|
||||||
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
|
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.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class GMItemEditViewModel(
|
class GMItemEditViewModel(
|
||||||
private val itemRepository: ItemRepository,
|
private val itemRepository: ItemRepository,
|
||||||
|
private val alterationRepository: AlterationRepository,
|
||||||
private val tagRepository: TagRepository,
|
private val tagRepository: TagRepository,
|
||||||
private val factory: GMItemEditFactory,
|
private val factory: GMItemEditFactory,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
|
|
@ -35,9 +38,11 @@ class GMItemEditViewModel(
|
||||||
private val _form = MutableStateFlow<GMItemEditForm?>(null)
|
private val _form = MutableStateFlow<GMItemEditForm?>(null)
|
||||||
val form: StateFlow<GMItemEditPageUio?> = _form.map {
|
val form: StateFlow<GMItemEditPageUio?> = _form.map {
|
||||||
if (it == null) return@map null
|
if (it == null) return@map null
|
||||||
factory.createForm(
|
factory.createPage(
|
||||||
form = it,
|
scope = viewModelScope,
|
||||||
originId = argument.id,
|
originId = argument.id,
|
||||||
|
form = it,
|
||||||
|
alterations = alterationRepository.alterationFlow.value
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
|
@ -50,12 +55,15 @@ class GMItemEditViewModel(
|
||||||
_form.value = factory.createForm(
|
_form.value = factory.createForm(
|
||||||
item = itemRepository.item(itemId = argument.id),
|
item = itemRepository.item(itemId = argument.id),
|
||||||
tags = tagRepository.itemsTags(),
|
tags = tagRepository.itemsTags(),
|
||||||
|
alterationFlow = alterationRepository.alterationFlow,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun save(): Boolean {
|
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 {
|
try {
|
||||||
itemRepository.updateItem(
|
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) {
|
fun addTag(tag: GMTagUio) {
|
||||||
_form.value?.tagFlow?.update { tags ->
|
_form.value?.tagFlow?.update { tags ->
|
||||||
tags.toMutableList().also {
|
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(
|
data class GMItemEditForm(
|
||||||
val idFlow: LwaTextFieldFlow,
|
val idFlow: LwaTextFieldFlow,
|
||||||
val labelFlow: LwaTextFieldFlow,
|
val labelFlow: LwaTextFieldFlow,
|
||||||
|
|
@ -97,6 +160,23 @@ class GMItemEditViewModel(
|
||||||
val stackableFlow: MutableStateFlow<Boolean>,
|
val stackableFlow: MutableStateFlow<Boolean>,
|
||||||
val equipableFlow: MutableStateFlow<Boolean>,
|
val equipableFlow: MutableStateFlow<Boolean>,
|
||||||
val consumableFlow: MutableStateFlow<Boolean>,
|
val consumableFlow: MutableStateFlow<Boolean>,
|
||||||
|
val alterationsFlow: MutableStateFlow<Map<String, GMAlterationFieldItemFlow>>,
|
||||||
val tagFlow: MutableStateFlow<List<GMTagUio>>,
|
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
|
@Immutable
|
||||||
@Stable
|
@Stable
|
||||||
data class ComputedPaddingValues(
|
data class ComputedPaddingValues(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue