Add Addable InventoryItem feature & quantity change.

This commit is contained in:
Thomas Andres Gomez 2025-04-19 19:37:55 +02:00
parent 48074f3d13
commit 9fce3f1cb8
28 changed files with 785 additions and 343 deletions

View file

@ -190,6 +190,9 @@
<string name="character__inventory__remove_from_purse__title">Retirer de la bourse</string>
<string name="character__inventory__filter_item_inventory__label">Filtrer les objets</string>
<string name="character__inventory__inventory__dialog__title">Ajouter à l'inventaire</string>
<string name="character__inventory__inventory__dialog__action">Ajouter à l'inventaire</string>
<string name="character__inventory__inventory__dialog__count">Quantité</string>
<string name="character__inventory__inventory__dialog__count_action">Modifier</string>
<string name="character__inventory__description_empty__label">Cet objet n'a pas de description.</string>
<string name="tooltip__characteristics__characteristics">Les caractéristiques constituent les aptitudes innées dun personnage comme son intelligence, sa force, son charisme, etc. Elles ne sont pas acquises, mais peuvent être parfois augmentées par un entraînement ou une utilisation réussie. Les caractéristiques des humains normaux varient de 2 (niveau extrêmement bas) à 20 (maximum du potentiel humain), avec une moyenne de 10 ou 11. Plus une caractéristique est élevée plus le personnage est puissant dans cette aptitude.\nÀ la création de votre personnage, répartissez les valeurs suivantes dans les différentes caractéristiques : 15, 15, 13, 11, 10, 9 et 7.</string>

View file

@ -111,10 +111,15 @@ interface LwaClient {
): APIResponse<InventoryJson>
suspend fun putInventory(
json: InventoryJson,
inventory: InventoryJson,
create: Boolean,
): APIResponse<Unit>
suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): APIResponse<String>
suspend fun deleteInventory(
characterSheetId: String,
): APIResponse<Unit>

View file

@ -192,6 +192,14 @@ class LwaClientImpl(
}
.body<APIResponse<Unit>>()
@Throws
override suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): APIResponse<String> = client
.put("$root/inventory/create?characterSheetId=$characterSheetId&itemId=$itemId")
.body<APIResponse<String>>()
@Throws
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.delete("$root/inventory/delete?characterSheetId=$characterSheetId")

View file

@ -2,27 +2,32 @@ package com.pixelized.desktop.lwa.repository.inventory
import com.pixelized.shared.lwa.model.inventory.Inventory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
class InventoryRepository(
private val inventoryStore: InventoryStore,
) {
val inventoryFlow get() = inventoryStore.inventories
suspend fun updateInventoryFlow(characterSheetId: String) {
inventoryStore.updateInventoryFlow(characterSheetId = characterSheetId)
}
fun inventoryFlow(): StateFlow<Map<String, Inventory>> {
return inventoryStore.inventories
}
fun inventory(
characterSheetId: String?,
): Inventory? {
return inventoryFlow.value[characterSheetId]
characterSheetId: String,
): Inventory {
return inventoryStore.inventories.value[characterSheetId]
?: Inventory.empty(characterSheetId)
}
fun inventoryFlow(
characterSheetId: String?,
): Flow<Inventory?> {
return inventoryFlow.map { it[characterSheetId] }
characterSheetId: String,
): Flow<Inventory> {
return inventoryStore.inventories
.map { it[characterSheetId] ?: Inventory.empty(characterSheetId) }
}
@Throws
@ -36,6 +41,17 @@ class InventoryRepository(
)
}
@Throws
suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): String {
return inventoryStore.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
}
@Throws
suspend fun deleteItem(
characterSheetId: String,

View file

@ -65,7 +65,7 @@ class InventoryStore(
create: Boolean,
) {
val request = client.putInventory(
json = factory.convertToJson(inventory = inventory),
inventory = factory.convertToJson(inventory = inventory),
create = create,
)
if (request.success.not()) {
@ -73,6 +73,21 @@ class InventoryStore(
}
}
@Throws
suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): String {
val request = client.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
if (request.success.not()) {
LwaClient.error(error = request)
}
return request.data!!
}
@Throws
suspend fun deleteInventory(
characterSheetId: String,

View file

@ -0,0 +1,78 @@
package com.pixelized.desktop.lwa.ui.composable.character
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
@Stable
object LwaDialogDefault {
val paddings = PaddingValues(vertical = 32.dp)
}
@Composable
fun <T> LwaDialog(
modifier: Modifier = Modifier,
paddings: PaddingValues = LwaDialogDefault.paddings,
color: Color = MaterialTheme.colors.surface,
state: State<T?>,
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
content: @Composable BoxScope.(T) -> Unit,
) {
state.value?.let { dialog ->
Dialog(
onDismissRequest = onDismissRequest,
content = {
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(
escape = onDismissRequest,
enter = onConfirm,
)
.fillMaxSize()
.padding(paddingValues = paddings)
.then(other = modifier),
contentAlignment = Alignment.Center,
) {
DecoratedBox(
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { },
)
) {
Surface(
color = color,
) {
Box {
content(dialog)
}
}
}
}
}
)
}
}

View file

@ -6,7 +6,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -14,7 +13,6 @@ 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.offset
import androidx.compose.foundation.layout.padding
@ -43,7 +41,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.image.DesaturatedAsyncImage
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
@ -51,7 +49,6 @@ import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout2
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__title
@ -103,31 +100,33 @@ object InventoryDialogItemDefault {
fun InventoryDialog(
dialog: State<InventoryDialogUio?>,
paddings: PaddingValues = InventoryDialogDefault.paddings,
spacing: Dp = InventoryDialogDefault.spacings,
onDismissRequest: () -> Unit,
onItem: (String) -> Unit,
onItem: (InventoryDialogUio, String) -> Unit,
) {
dialog.value?.let {
Dialog(
LwaDialog(
state = dialog,
onDismissRequest = onDismissRequest,
onConfirm = onDismissRequest,
) {
InventoryDialogContent(
dialog = it,
paddings = paddings,
spacing = spacing,
onDismissRequest = onDismissRequest,
content = {
InventoryDialogContent(
dialog = it,
paddings = paddings,
onDismissRequest = onDismissRequest,
onItem = onItem,
)
}
onItem = onItem,
)
}
}
@Composable
private fun InventoryDialogContent(
modifier: Modifier = Modifier,
dialog: InventoryDialogUio,
paddings: PaddingValues = InventoryDialogDefault.paddings,
spacing: Dp = InventoryDialogDefault.spacings,
onDismissRequest: () -> Unit,
onItem: (String) -> Unit,
onItem: (InventoryDialogUio, String) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val start = remember(layoutDirection) { paddings.calculateStartPadding(layoutDirection) }
@ -135,98 +134,80 @@ private fun InventoryDialogContent(
val top = remember(layoutDirection) { paddings.calculateTopPadding() }
val bottom = remember(layoutDirection) { paddings.calculateBottomPadding() }
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(
escape = onDismissRequest,
enter = onDismissRequest,
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,
Column(
modifier = modifier,
) {
DecoratedBox {
Surface {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = top, start = start, bottom = spacing, end = end),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = top, start = start, bottom = spacing, end = end),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.weight(weight = 1f),
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character__inventory__inventory__dialog__title),
)
IconButton(
modifier = Modifier.offset(x = end, y = -top),
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
LwaTextField(
colors = LwaTextFieldColors(backgroundColor = Color.Transparent),
modifier = Modifier.fillMaxWidth(),
field = dialog.filter,
trailingIcon = {
val value = dialog.filter.valueFlow.collectAsState()
AnimatedVisibility(
visible = value.value.isNotBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = { dialog.filter.onValueChange.invoke("") },
) {
Text(
modifier = Modifier.weight(weight = 1f),
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character__inventory__inventory__dialog__title),
Icon(
painter = painterResource(Res.drawable.ic_cancel_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
IconButton(
modifier = Modifier.offset(x = end, y = -top),
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
LwaTextField(
colors = LwaTextFieldColors(backgroundColor = Color.Transparent),
modifier = Modifier.fillMaxWidth(),
field = dialog.filter,
trailingIcon = {
val value = dialog.filter.valueFlow.collectAsState()
AnimatedVisibility(
visible = value.value.isNotBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = { dialog.filter.onValueChange.invoke("") },
) {
Icon(
painter = painterResource(Res.drawable.ic_cancel_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
}
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 1f),
contentPadding = PaddingValues(
start = start,
top = spacing,
end = end,
bottom = bottom
),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = dialog.items,
key = { it.itemId },
) { item ->
InventoryDialogItem(
modifier = Modifier.animateItem(),
item = item,
onItem = onItem,
)
}
}
}
}
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 1f),
contentPadding = PaddingValues(
start = start,
top = spacing,
end = end,
bottom = bottom
),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = dialog.items,
key = { it.itemId },
) { item ->
InventoryDialogItem(
modifier = Modifier.animateItem(),
item = item,
onItem = { onItem(dialog, it) },
)
}
}
}
}

View file

@ -1,26 +1,28 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
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.ui.Alignment
import androidx.compose.ui.Modifier
@ -29,135 +31,164 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
import com.pixelized.desktop.lwa.ui.composable.image.DesaturatedAsyncImage
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__count_action
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class ItemDetailDialogUio(
val characterSheetId: String,
val inventoryId: String?,
val itemId: String,
// meta
val label: String,
val description: String,
val image: String?,
val option: OptionUio,
) {
@Stable
data class OptionUio(
val equipable: Boolean,
val consumable: Boolean,
)
}
val count: Float,
// options
val countable: LwaTextFieldUio?,
)
@Stable
object ItemDetailDialogDefault {
@Stable
val paddings = PaddingValues(all = 16.dp)
@Stable
val spacings = 8.dp
}
@Composable
fun ItemDetailDialog(
dialog: State<ItemDetailDialogUio?>,
paddings: PaddingValues = ItemDetailDialogDefault.paddings,
spacings: Dp = ItemDetailDialogDefault.spacings,
onDismissRequest: () -> Unit,
onConfirm: (ItemDetailDialogUio) -> Unit,
onAddItem: (ItemDetailDialogUio) -> Unit,
) {
dialog.value?.let {
Dialog(
onDismissRequest = onDismissRequest,
content = {
ItemDetailDialogContent(
dialog = it,
paddings = paddings,
onDismissRequest = onDismissRequest,
LwaDialog(
state = dialog,
onDismissRequest = onDismissRequest,
onConfirm = { dialog.value?.let(onConfirm) ?: onDismissRequest },
) { state ->
val layoutDirection = LocalLayoutDirection.current
val end = remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) }
val top = remember(paddings) { paddings.calculateTopPadding() }
takeIf { state.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage(
modifier = Modifier
.size(64.dp * 2)
.align(alignment = Alignment.TopEnd),
colorFilter = rememberSaturationFilter(),
model = state.image,
contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
Column(
modifier = Modifier.padding(paddingValues = paddings),
verticalArrangement = Arrangement.spacedBy(space = spacings)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.weight(weight = 1f),
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = state.label,
)
}
)
}
}
@Composable
private fun ItemDetailDialogContent(
modifier: Modifier = Modifier,
paddings: PaddingValues,
dialog: ItemDetailDialogUio,
onDismissRequest: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val end = remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) }
val top = remember(paddings) { paddings.calculateTopPadding() }
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(
escape = onDismissRequest,
enter = onDismissRequest,
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,
) {
DecoratedBox {
Surface {
Box(
modifier = Modifier.padding(paddingValues = paddings)
IconButton(
modifier = Modifier.offset(x = end, y = -top),
onClick = onDismissRequest,
) {
takeIf { dialog.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage(
modifier = Modifier
.size(64.dp * 2)
.align(alignment = Alignment.TopEnd),
colorFilter = rememberSaturationFilter(),
model = dialog.image,
contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
Text(
style = MaterialTheme.typography.body1,
text = state.description,
)
Spacer(modifier = Modifier)
AnimatedContent(
targetState = state.inventoryId,
) {
when (it) {
null -> Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
TextButton(
onClick = { onAddItem(state) },
) {
Text(
modifier = Modifier.weight(weight = 1f),
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = dialog.label,
text = stringResource(Res.string.character__inventory__inventory__dialog__action)
)
IconButton(
modifier = Modifier.offset(x = end, y = -top),
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
}
}
else -> Column(
verticalArrangement = Arrangement.spacedBy(space = spacings)
) {
if (state.countable != null) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
space = spacings,
Alignment.End,
)
) {
LwaTextField(
modifier = Modifier.width(width = 128.dp),
colors = LwaTextFieldColors(
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base2dp,
),
field = state.countable,
)
TextButton(
modifier = Modifier
.height(height = 56.dp)
.background(
color = MaterialTheme.lwa.colorScheme.elevated.base2dp,
shape = MaterialTheme.shapes.small,
),
enabled = state.countable.isError.collectAsState().value.not(),
onClick = { onConfirm(state) }
) {
Text(
text = stringResource(Res.string.character__inventory__inventory__dialog__count_action),
)
}
}
}
Text(
style = MaterialTheme.typography.body1,
text = dialog.description,
)
}
}
}

View file

@ -1,20 +1,31 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.shared.lwa.model.item.Item
import kotlinx.coroutines.flow.MutableStateFlow
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__description_empty__label
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__count
import org.jetbrains.compose.resources.getString
import java.text.DecimalFormat
class ItemDetailDialogFactory {
private val floatChecker = Regex("""^\d*[.,]?\d*${'$'}""")
private val format = DecimalFormat("#.##")
suspend fun convertToDialogUio(
characterSheetId: String?,
items: Map<String, Item>,
count: Float,
inventoryId: String?,
itemId: String?,
): ItemDetailDialogUio? {
if (characterSheetId == null) return null
val item = itemId.let(items::get) ?: return null
return ItemDetailDialogUio(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
itemId = item.id,
label = item.metadata.label,
@ -22,10 +33,42 @@ class ItemDetailDialogFactory {
getString(Res.string.character__inventory__description_empty__label)
},
image = item.metadata.image,
option = ItemDetailDialogUio.OptionUio(
equipable = item.options.equipable,
consumable = item.options.consumable,
),
count = count,
countable = takeIf { item.options.stackable }
?.let { createFieldFlow(value = format.format(count)) }
?.createTextField(label = getString(Res.string.character__inventory__inventory__dialog__count)),
)
}
private fun createFieldFlow(
value: String = "",
error: Boolean = false,
): Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>> {
return MutableStateFlow(value) to MutableStateFlow(error)
}
fun parse(
quantity: String,
): Float? = try {
format.parse(quantity).toFloat()
} catch (_: Exception) {
null
}
private fun isError(value: String): Boolean = floatChecker.matches(value).not()
private fun Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>>.createTextField(
enable: Boolean = true,
label: String,
) = LwaTextFieldUio(
enable = enable,
isError = second,
valueFlow = first,
label = label,
placeHolder = null,
onValueChange = {
second.value = isError(value = it)
first.value = it
},
)
}

View file

@ -2,24 +2,56 @@ package com.pixelized.desktop.lwa.ui.composable.character.item
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.shared.lwa.model.inventory.Inventory
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class ItemDetailDialogViewModel(
private val inventoryRepository: InventoryRepository,
itemRepository: ItemRepository,
factory: ItemDetailDialogFactory,
private val factory: ItemDetailDialogFactory,
) : ViewModel() {
private val _error = MutableSharedFlow<ErrorSnackUio>()
val error: SharedFlow<ErrorSnackUio> = _error
private val selectedItemId = MutableStateFlow<InventoryItemId?>(null)
@OptIn(ExperimentalCoroutinesApi::class)
private val selectedInventoryItem: Flow<Inventory.Item?> = selectedItemId
.flatMapLatest { selectedIds ->
when (selectedIds?.inventoryId) {
null -> flowOf(null)
else -> inventoryRepository
.inventoryFlow(characterSheetId = selectedIds.characterSheetId)
.mapNotNull { inventory ->
inventory.items.firstOrNull { it.inventoryId == selectedIds.inventoryId }
}
}
}
val itemDialog = combine(
itemRepository.itemFlow(), selectedItemId,
transform = { items, ids ->
itemRepository.itemFlow(),
selectedInventoryItem,
selectedItemId,
transform = { items, selectedInventoryItem, ids ->
factory.convertToDialogUio(
characterSheetId = ids?.characterSheetId,
items = items,
count = selectedInventoryItem?.count ?: 0f,
inventoryId = ids?.inventoryId,
itemId = ids?.itemId,
)
@ -30,9 +62,14 @@ class ItemDetailDialogViewModel(
initialValue = null
)
fun showItemDialog(inventoryId: String?, itemId: String?) {
fun showItemDialog(
characterSheetId: String,
inventoryId: String?,
itemId: String?,
) {
selectedItemId.update {
InventoryItemId(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
itemId = itemId,
)
@ -43,7 +80,63 @@ class ItemDetailDialogViewModel(
selectedItemId.update { null }
}
suspend fun onAddInventoryItem(
characterSheetId: String,
itemId: String,
) {
try {
// create the inventory item on the server, get the newly create id from that.
val inventoryId = inventoryRepository.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
// update the dialog with the id only if this dialog still correspond to this item. (should always be the case but hey).
if (selectedItemId.value?.let { it.itemId == itemId && it.characterSheetId == characterSheetId } == true) {
selectedItemId.update {
it?.copy(inventoryId = inventoryId)
}
}
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception)
_error.emit(message)
}
}
suspend fun changeInventoryItemQuantity(
dialog: ItemDetailDialogUio
) : Boolean {
if (dialog.countable?.isError?.value == true) return false
val characterSheetId = dialog.characterSheetId
val inventoryId = dialog.inventoryId ?: return false
val quantity = dialog.countable?.valueFlow?.value ?: return false
val count = factory.parse(quantity = quantity) ?: return false
val inventory = inventoryRepository.inventory(characterSheetId = characterSheetId)
try {
inventoryRepository.updateInventory(
inventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
val index = items.indexOfFirst { item -> item.inventoryId == inventoryId }
items[index] = items[index].copy(
count = count,
)
}
),
create = false,
)
return true
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception)
_error.emit(message)
return false
}
}
private data class InventoryItemId(
val characterSheetId: String,
val inventoryId: String?,
val itemId: String?,
)

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -39,8 +40,11 @@ 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.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
@ -66,27 +70,103 @@ data class PurseDialogUio(
val enableConfirm: StateFlow<Boolean>,
)
@Stable
object PurseDialogDefault {
@Stable
val paddings = PaddingValues(top = 16.dp, start = 8.dp, end = 8.dp, bottom = 8.dp)
@Stable
val spacings = DpSize(width = 4.dp, height = 8.dp)
}
@Composable
fun PurseDialog(
dialog: State<PurseDialogUio?>,
paddings: PaddingValues = PurseDialogDefault.paddings,
spacings: DpSize = PurseDialogDefault.spacings,
onConfirm: (PurseDialogUio) -> Unit,
onSwapSign: (PurseDialogUio) -> Unit,
onDismissRequest: () -> Unit,
) {
dialog.value?.let {
Dialog(
onDismissRequest = onDismissRequest,
content = {
PurseContent(
dialog = it,
onConfirm = onConfirm,
onSwapSign = onSwapSign,
onDismissRequest = onDismissRequest,
)
PurseDialogKeyHandler(
onSwap = { onSwapSign(it) },
LwaDialog(
state = dialog,
onDismissRequest = onDismissRequest,
onConfirm = { dialog.value?.let(onConfirm) }
) { state ->
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() }
Column(
modifier = Modifier.padding(paddingValues = paddings),
verticalArrangement = Arrangement.spacedBy(space = spacings.height),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedContent(
modifier = Modifier,
targetState = state.label.collectAsState().value,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) {
Text(
style = MaterialTheme.typography.caption,
text = it,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = spacings.width),
verticalAlignment = Alignment.Bottom,
) {
SignButton(
modifier = Modifier
.size(size = 56.dp)
.background(
color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
shape = MaterialTheme.shapes.small,
),
add = state.add,
onClick = { onSwapSign(state) },
)
LwaTextField(
modifier = Modifier.focusRequester(focusRequester = focusRequester)
.width(100.dp),
field = state.gold,
)
LwaTextField(
modifier = Modifier.width(100.dp),
field = state.silver,
)
LwaTextField(
modifier = Modifier.width(100.dp),
field = state.copper,
)
}
Row(
modifier = Modifier.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = spacings.width,
alignment = Alignment.End
)
) {
TextButton(
onClick = onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primary.copy(alpha = .7f),
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
enabled = state.enableConfirm.collectAsState().value,
onClick = { onConfirm(state) },
) {
Text(
text = stringResource(Res.string.dialog__confirm_action)
)
}
}
}
PurseDialogKeyHandler(
onSwap = { onSwapSign(state) },
)
}
}
@ -99,8 +179,7 @@ private fun PurseContent(
onSwapSign: (PurseDialogUio) -> Unit,
onDismissRequest: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() }
Box(
modifier = Modifier
@ -119,76 +198,7 @@ private fun PurseContent(
) {
DecoratedBox {
Surface {
Column(
modifier = modifier.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedContent(
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
targetState = dialog.label.collectAsState().value,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) {
Text(
style = MaterialTheme.typography.caption,
text = it,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.Bottom,
) {
SignButton(
modifier = Modifier
.size(size = 56.dp)
.background(
color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
shape = MaterialTheme.shapes.small,
),
add = dialog.add,
onClick = { onSwapSign(dialog) },
)
LwaTextField(
modifier = Modifier.focusRequester(focusRequester = focusRequester)
.width(100.dp),
field = dialog.gold,
)
LwaTextField(
modifier = Modifier.width(100.dp),
field = dialog.silver,
)
LwaTextField(
modifier = Modifier.width(100.dp),
field = dialog.copper,
)
}
Row(
modifier = Modifier
.padding(bottom = 4.dp)
.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = 4.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primary.copy(alpha = .7f),
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
enabled = dialog.enableConfirm.collectAsState().value,
onClick = { onConfirm(dialog) },
) {
Text(
text = stringResource(Res.string.dialog__confirm_action)
)
}
}
}
}
}
}

View file

@ -56,9 +56,7 @@ class PurseDialogViewModel(
return false
}
// Get the player inventory
val inventory = inventoryRepository
.inventory(characterSheetId = dialog.characterSheetId)
?: return false
val inventory = inventoryRepository.inventory(characterSheetId = dialog.characterSheetId)
// compute the new purse
val sign = if (dialog.add.value) 1 else -1
val goldValue = dialog.gold.valueFlow.value.toIntOrNull() ?: 0

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
@ -27,6 +28,7 @@ fun <T> TooltipLayout2(
tooltip = {
Box(
modifier = Modifier.width(width = 448.dp),
contentAlignment = Alignment.TopEnd,
content = { tooltip(tips) },
)
},

View file

@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil3.compose.rememberDrawScopeSizeResolver
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialog
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogViewModel
@ -104,11 +105,12 @@ fun CharacterDetailInventory(
characterSheetId = it,
)
},
onItem = {
onItem = { item ->
blur.show()
itemDetailDialogViewModel.showItemDialog(
inventoryId = it.inventoryId,
itemId = it.itemId,
characterSheetId = item.characterSheetId,
inventoryId = item.inventoryId,
itemId = item.itemId,
)
},
onAddItem = {
@ -145,9 +147,10 @@ fun CharacterDetailInventory(
blur.hide()
inventoryDialogViewModel.hideInventoryDialog()
},
onItem = { itemId ->
onItem = { dialog, itemId ->
blur.show()
itemDetailDialogViewModel.showItemDialog(
characterSheetId = dialog.characterSheetId,
inventoryId = null,
itemId = itemId,
)
@ -159,6 +162,25 @@ fun CharacterDetailInventory(
onDismissRequest = {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
},
onAddItem = { dialog ->
scope.launch {
itemDetailDialogViewModel.onAddInventoryItem(
characterSheetId = dialog.characterSheetId,
itemId = dialog.itemId,
)
}
},
onConfirm = { dialog ->
scope.launch {
val result = itemDetailDialogViewModel.changeInventoryItemQuantity(
dialog = dialog,
)
if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
}
}
}
)
@ -232,9 +254,7 @@ private fun CharacterDetailInventoryContent(
key = { it.inventoryId },
) {
InventoryItem(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
modifier = Modifier.animateItem(),
item = it,
onClick = { onItem(it) },
)

View file

@ -80,6 +80,7 @@ class CharacterDetailInventoryFactory(
?.mapNotNull {
val item = items[it.itemId] ?: return@mapNotNull null
InventoryItemUio(
characterSheetId = characterSheetId,
inventoryId = it.inventoryId,
itemId = it.itemId,
label = item.metadata.label,

View file

@ -12,6 +12,7 @@ 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.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -40,6 +41,7 @@ import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
@Stable
data class InventoryItemUio(
val characterSheetId: String,
val inventoryId: String,
val itemId: String,
val label: String,
@ -74,6 +76,7 @@ fun InventoryItem(
onClick: () -> Unit,
) {
TooltipLayout2(
modifier = modifier,
delayMillis = 500,
tips = item.tooltips,
tooltip = { tooltips ->
@ -97,7 +100,7 @@ fun InventoryItem(
)
}
Column(
modifier = modifier,
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
@ -128,6 +131,7 @@ fun InventoryItem(
}
)
.minimumInteractiveComponentSize()
.fillMaxWidth()
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = spacing),

View file

@ -49,4 +49,15 @@ class InventoryService(
fun delete(characterSheetId: String) {
inventoryStore.delete(characterSheetId = characterSheetId)
}
@Throws
fun createItem(
characterSheetId: String,
itemId: String,
) : String {
return inventoryStore.createItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
}
}

View file

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File
import java.util.UUID
class InventoryStore(
private val pathProvider: PathProvider,
@ -152,7 +153,49 @@ class InventoryStore(
}
}
@Throws
fun createItem(
characterSheetId: String,
itemId: String,
): String {
val (updatedInventory, inventoryId) = inventoryFlow.value.toMutableMap().let { characters ->
// get the inventory of the character, if none create one.
val inventory = characters[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// create an inventoryId.
val inventoryId = inventory.items.createInventoryId()
// create an inventory Item.
val item = Inventory.Item(
inventoryId = inventoryId,
itemId = itemId,
count = 1f,
equipped = false,
)
// update the inventory with the updated item.
inventory.copy(
items = inventory.items.toMutableList().also {
it.add(item)
}
) to inventoryId
}
// save the inventory
save(
inventory = updatedInventory,
create = false,
)
// return the inventory ID.
return inventoryId
}
private fun inventoryFile(id: String): File {
return File("${pathProvider.inventoryPath()}${id}.json")
}
private fun List<Inventory.Item>.createInventoryId(): String {
var inventoryId: String
do {
inventoryId = UUID.randomUUID().toString()
} while (any { inventoryId == it.inventoryId })
return inventoryId
}
}

View file

@ -19,6 +19,7 @@ import com.pixelized.server.lwa.server.rest.character.putCharacterAlteration
import com.pixelized.server.lwa.server.rest.character.putCharacterDamage
import com.pixelized.server.lwa.server.rest.character.putCharacterDiminished
import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue
import com.pixelized.server.lwa.server.rest.inventory.createInventory
import com.pixelized.server.lwa.server.rest.inventory.deleteInventory
import com.pixelized.server.lwa.server.rest.inventory.getInventory
import com.pixelized.server.lwa.server.rest.inventory.putInventory
@ -258,6 +259,10 @@ class LocalServer {
path = "/update",
body = engine.putInventory()
)
put(
path = "/create",
body = engine.createInventory()
)
delete(
path = "/delete",
body = engine.deleteInventory()

View file

@ -1,4 +1,6 @@
package com.pixelized.server.lwa.server.exception
class MissingParameterException(name: String) :
import com.pixelized.shared.lwa.protocol.rest.APIResponse.ErrorCode
class MissingParameterException(name: String, val code: ErrorCode) :
ServerException(root = Exception("Missing '$name' parameter."))

View file

@ -1,9 +1,9 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.param
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -14,8 +14,10 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val damage = call.queryParameters["damage"]?.toIntOrNull()
?: throw MissingParameterException(name = "damage")
val damage: Int = call.queryParameters.param(
name = "damage",
code = APIResponse.ErrorCode.Damage,
)
// fetch the character sheet
val characterSheet = characterService.character(characterSheetId)
?: error("CharacterSheet with id:$characterSheetId not found.")

View file

@ -1,9 +1,9 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.param
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -14,8 +14,10 @@ fun Engine.putCharacterDiminished(): suspend RoutingContext.() -> Unit {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val diminished = call.queryParameters["diminished"]?.toIntOrNull()
?: throw MissingParameterException(name = "diminished")
val diminished: Int = call.queryParameters.param(
name = "diminished",
code = APIResponse.ErrorCode.Diminished,
)
// Update the character damage
characterService.updateDiminished(
characterSheetId = characterSheetId,

View file

@ -4,6 +4,7 @@ import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.param
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -14,8 +15,10 @@ fun Engine.putCharacterFatigue(): suspend RoutingContext.() -> Unit {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val fatigue = call.queryParameters["fatigue"]?.toIntOrNull()
?: throw MissingParameterException(name = "fatigue")
val fatigue: Int = call.queryParameters.param(
name = "fatigue",
code = APIResponse.ErrorCode.Fatigue,
)
// fetch the character sheet
val characterSheet = characterService.character(characterSheetId)
?: error("CharacterSheet with id:$characterSheetId not found.")

View file

@ -0,0 +1,41 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.itemId
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.createInventory(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val itemId = call.queryParameters.itemId
// add the item to the inventory.
val inventoryId = inventoryService.createItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(
data = inventoryId,
),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -13,7 +13,7 @@ fun Engine.deleteInventory(): suspend RoutingContext.() -> Unit {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// delete the alteration.
// delete the inventory.
inventoryService.delete(
characterSheetId = characterSheetId,
)

View file

@ -1,29 +1,49 @@
package com.pixelized.server.lwa.utils.extentions
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.http.Parameters
val Parameters.characterSheetId
get() = "characterSheetId".let { param ->
this[param] ?: throw MissingParameterException(name = param)
}
inline fun <reified T> Parameters.param(
name: String,
code: APIResponse.ErrorCode,
): T {
return when (T::class) {
String::class -> this[name] as? T
Boolean::class -> this[name]?.toBooleanStrictOrNull() as? T
else -> null
} ?: throw MissingParameterException(
name = name,
code = code,
)
}
val Parameters.alterationId
get() = "alterationId".let { param ->
this[param] ?: throw MissingParameterException(name = param)
}
val Parameters.characterSheetId: String
get() = param(
name = "characterSheetId",
code = APIResponse.ErrorCode.CharacterSheetId,
)
val Parameters.itemId
get() = "itemId".let { param ->
this[param] ?: throw MissingParameterException(name = param)
}
val Parameters.alterationId: String
get() = param(
name = "alterationId",
code = APIResponse.ErrorCode.AlterationId,
)
val Parameters.create
get() = "create".let { param ->
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = param)
}
val Parameters.itemId: String
get() = param(
name = "itemId",
code = APIResponse.ErrorCode.ItemId,
)
val Parameters.active
get() = "active".let { param ->
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = param)
}
val Parameters.create: Boolean
get() = param(
name = "create",
code = APIResponse.ErrorCode.Create,
)
val Parameters.active: Boolean
get() = param(
name = "active",
code = APIResponse.ErrorCode.Active,
)

View file

@ -15,7 +15,7 @@ suspend inline fun <reified T : Exception> RoutingCall.exception(exception: T) {
message = APIResponse.error(
status = APIResponse.BAD_REQUEST,
message = exception.message ?: "?",
code = APIResponse.ErrorCode.AlterationName,
code = exception.code,
)
)
}

View file

@ -17,6 +17,11 @@ data class APIResponse<T>(
ItemId,
ItemName,
CharacterSheetId,
Create,
Active,
Damage,
Fatigue,
Diminished,
}
companion object {