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__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__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__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="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> <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> ): APIResponse<InventoryJson>
suspend fun putInventory( suspend fun putInventory(
json: InventoryJson, inventory: InventoryJson,
create: Boolean, create: Boolean,
): APIResponse<Unit> ): APIResponse<Unit>
suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): APIResponse<String>
suspend fun deleteInventory( suspend fun deleteInventory(
characterSheetId: String, characterSheetId: String,
): APIResponse<Unit> ): APIResponse<Unit>

View file

@ -192,6 +192,14 @@ class LwaClientImpl(
} }
.body<APIResponse<Unit>>() .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 @Throws
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.delete("$root/inventory/delete?characterSheetId=$characterSheetId") .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 com.pixelized.shared.lwa.model.inventory.Inventory
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class InventoryRepository( class InventoryRepository(
private val inventoryStore: InventoryStore, private val inventoryStore: InventoryStore,
) { ) {
val inventoryFlow get() = inventoryStore.inventories
suspend fun updateInventoryFlow(characterSheetId: String) { suspend fun updateInventoryFlow(characterSheetId: String) {
inventoryStore.updateInventoryFlow(characterSheetId = characterSheetId) inventoryStore.updateInventoryFlow(characterSheetId = characterSheetId)
} }
fun inventoryFlow(): StateFlow<Map<String, Inventory>> {
return inventoryStore.inventories
}
fun inventory( fun inventory(
characterSheetId: String?, characterSheetId: String,
): Inventory? { ): Inventory {
return inventoryFlow.value[characterSheetId] return inventoryStore.inventories.value[characterSheetId]
?: Inventory.empty(characterSheetId)
} }
fun inventoryFlow( fun inventoryFlow(
characterSheetId: String?, characterSheetId: String,
): Flow<Inventory?> { ): Flow<Inventory> {
return inventoryFlow.map { it[characterSheetId] } return inventoryStore.inventories
.map { it[characterSheetId] ?: Inventory.empty(characterSheetId) }
} }
@Throws @Throws
@ -36,6 +41,17 @@ class InventoryRepository(
) )
} }
@Throws
suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): String {
return inventoryStore.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
}
@Throws @Throws
suspend fun deleteItem( suspend fun deleteItem(
characterSheetId: String, characterSheetId: String,

View file

@ -65,7 +65,7 @@ class InventoryStore(
create: Boolean, create: Boolean,
) { ) {
val request = client.putInventory( val request = client.putInventory(
json = factory.convertToJson(inventory = inventory), inventory = factory.convertToJson(inventory = inventory),
create = create, create = create,
) )
if (request.success.not()) { 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 @Throws
suspend fun deleteInventory( suspend fun deleteInventory(
characterSheetId: String, 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.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding 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.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.image.DesaturatedAsyncImage 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.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.composable.tooltip.TooltipLayout2
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__title import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__title
@ -103,31 +100,33 @@ object InventoryDialogItemDefault {
fun InventoryDialog( fun InventoryDialog(
dialog: State<InventoryDialogUio?>, dialog: State<InventoryDialogUio?>,
paddings: PaddingValues = InventoryDialogDefault.paddings, paddings: PaddingValues = InventoryDialogDefault.paddings,
spacing: Dp = InventoryDialogDefault.spacings,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onItem: (String) -> Unit, onItem: (InventoryDialogUio, String) -> Unit,
) { ) {
dialog.value?.let { LwaDialog(
Dialog( state = dialog,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
content = { onConfirm = onDismissRequest,
) {
InventoryDialogContent( InventoryDialogContent(
dialog = it, dialog = it,
paddings = paddings, paddings = paddings,
spacing = spacing,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onItem = onItem, onItem = onItem,
) )
} }
)
}
} }
@Composable @Composable
private fun InventoryDialogContent( private fun InventoryDialogContent(
modifier: Modifier = Modifier,
dialog: InventoryDialogUio, dialog: InventoryDialogUio,
paddings: PaddingValues = InventoryDialogDefault.paddings, paddings: PaddingValues = InventoryDialogDefault.paddings,
spacing: Dp = InventoryDialogDefault.spacings, spacing: Dp = InventoryDialogDefault.spacings,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onItem: (String) -> Unit, onItem: (InventoryDialogUio, String) -> Unit,
) { ) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val start = remember(layoutDirection) { paddings.calculateStartPadding(layoutDirection) } val start = remember(layoutDirection) { paddings.calculateStartPadding(layoutDirection) }
@ -135,24 +134,9 @@ private fun InventoryDialogContent(
val top = remember(layoutDirection) { paddings.calculateTopPadding() } val top = remember(layoutDirection) { paddings.calculateTopPadding() }
val bottom = remember(layoutDirection) { paddings.calculateBottomPadding() } val bottom = remember(layoutDirection) { paddings.calculateBottomPadding() }
Box( Column(
modifier = Modifier 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 {
Column {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -221,14 +205,11 @@ private fun InventoryDialogContent(
InventoryDialogItem( InventoryDialogItem(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
item = item, item = item,
onItem = onItem, onItem = { onItem(dialog, it) },
) )
} }
} }
} }
}
}
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)

View file

@ -1,26 +1,28 @@
package com.pixelized.desktop.lwa.ui.composable.character.item package com.pixelized.desktop.lwa.ui.composable.character.item
import androidx.compose.foundation.clickable import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
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.Spacer
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
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.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton
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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -29,106 +31,80 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
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.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.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res 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 lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable @Stable
data class ItemDetailDialogUio( data class ItemDetailDialogUio(
val characterSheetId: String,
val inventoryId: String?, val inventoryId: String?,
val itemId: String, val itemId: String,
// meta
val label: String, val label: String,
val description: String, val description: String,
val image: String?, val image: String?,
val option: OptionUio, val count: Float,
) { // options
@Stable val countable: LwaTextFieldUio?,
data class OptionUio( )
val equipable: Boolean,
val consumable: Boolean,
)
}
@Stable @Stable
object ItemDetailDialogDefault { object ItemDetailDialogDefault {
@Stable @Stable
val paddings = PaddingValues(all = 16.dp) val paddings = PaddingValues(all = 16.dp)
@Stable
val spacings = 8.dp
} }
@Composable @Composable
fun ItemDetailDialog( fun ItemDetailDialog(
dialog: State<ItemDetailDialogUio?>, dialog: State<ItemDetailDialogUio?>,
paddings: PaddingValues = ItemDetailDialogDefault.paddings, paddings: PaddingValues = ItemDetailDialogDefault.paddings,
spacings: Dp = ItemDetailDialogDefault.spacings,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: (ItemDetailDialogUio) -> Unit,
onAddItem: (ItemDetailDialogUio) -> Unit,
) { ) {
dialog.value?.let { LwaDialog(
Dialog( state = dialog,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
content = { onConfirm = { dialog.value?.let(onConfirm) ?: onDismissRequest },
ItemDetailDialogContent( ) { state ->
dialog = it,
paddings = paddings,
onDismissRequest = onDismissRequest,
)
}
)
}
}
@Composable
private fun ItemDetailDialogContent(
modifier: Modifier = Modifier,
paddings: PaddingValues,
dialog: ItemDetailDialogUio,
onDismissRequest: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val end = remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) } val end = remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) }
val top = remember(paddings) { paddings.calculateTopPadding() } val top = remember(paddings) { paddings.calculateTopPadding() }
Box( takeIf { state.image?.isNotEmpty() == true }?.let {
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)
) {
takeIf { dialog.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage( DesaturatedAsyncImage(
modifier = Modifier modifier = Modifier
.size(64.dp * 2) .size(64.dp * 2)
.align(alignment = Alignment.TopEnd), .align(alignment = Alignment.TopEnd),
colorFilter = rememberSaturationFilter(), colorFilter = rememberSaturationFilter(),
model = dialog.image, model = state.image,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter, alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High, filterQuality = FilterQuality.High,
contentDescription = null, contentDescription = null,
) )
} }
Column( Column(
modifier = modifier, modifier = Modifier.padding(paddingValues = paddings),
verticalArrangement = Arrangement.spacedBy(space = 8.dp) verticalArrangement = Arrangement.spacedBy(space = spacings)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -141,7 +117,7 @@ private fun ItemDetailDialogContent(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
text = dialog.label, text = state.label,
) )
IconButton( IconButton(
modifier = Modifier.offset(x = end, y = -top), modifier = Modifier.offset(x = end, y = -top),
@ -154,13 +130,68 @@ private fun ItemDetailDialogContent(
) )
} }
} }
Text( Text(
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
text = dialog.description, text = state.description,
)
Spacer(modifier = Modifier)
AnimatedContent(
targetState = state.inventoryId,
) {
when (it) {
null -> Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(
onClick = { onAddItem(state) },
) {
Text(
text = stringResource(Res.string.character__inventory__inventory__dialog__action)
)
}
}
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),
) )
} }
} }
} }
} }
} }
}
}
}
} }

View file

@ -1,20 +1,31 @@
package com.pixelized.desktop.lwa.ui.composable.character.item 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 com.pixelized.shared.lwa.model.item.Item
import kotlinx.coroutines.flow.MutableStateFlow
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__description_empty__label 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 org.jetbrains.compose.resources.getString
import java.text.DecimalFormat
class ItemDetailDialogFactory { class ItemDetailDialogFactory {
private val floatChecker = Regex("""^\d*[.,]?\d*${'$'}""")
private val format = DecimalFormat("#.##")
suspend fun convertToDialogUio( suspend fun convertToDialogUio(
characterSheetId: String?,
items: Map<String, Item>, items: Map<String, Item>,
count: Float,
inventoryId: String?, inventoryId: String?,
itemId: String?, itemId: String?,
): ItemDetailDialogUio? { ): ItemDetailDialogUio? {
if (characterSheetId == null) return null
val item = itemId.let(items::get) ?: return null val item = itemId.let(items::get) ?: return null
return ItemDetailDialogUio( return ItemDetailDialogUio(
characterSheetId = characterSheetId,
inventoryId = inventoryId, inventoryId = inventoryId,
itemId = item.id, itemId = item.id,
label = item.metadata.label, label = item.metadata.label,
@ -22,10 +33,42 @@ class ItemDetailDialogFactory {
getString(Res.string.character__inventory__description_empty__label) getString(Res.string.character__inventory__description_empty__label)
}, },
image = item.metadata.image, image = item.metadata.image,
option = ItemDetailDialogUio.OptionUio( count = count,
equipable = item.options.equipable, countable = takeIf { item.options.stackable }
consumable = item.options.consumable, ?.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.ViewModel
import androidx.lifecycle.viewModelScope 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.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.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine 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.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
class ItemDetailDialogViewModel( class ItemDetailDialogViewModel(
private val inventoryRepository: InventoryRepository,
itemRepository: ItemRepository, itemRepository: ItemRepository,
factory: ItemDetailDialogFactory, private val factory: ItemDetailDialogFactory,
) : ViewModel() { ) : ViewModel() {
private val _error = MutableSharedFlow<ErrorSnackUio>()
val error: SharedFlow<ErrorSnackUio> = _error
private val selectedItemId = MutableStateFlow<InventoryItemId?>(null) 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( val itemDialog = combine(
itemRepository.itemFlow(), selectedItemId, itemRepository.itemFlow(),
transform = { items, ids -> selectedInventoryItem,
selectedItemId,
transform = { items, selectedInventoryItem, ids ->
factory.convertToDialogUio( factory.convertToDialogUio(
characterSheetId = ids?.characterSheetId,
items = items, items = items,
count = selectedInventoryItem?.count ?: 0f,
inventoryId = ids?.inventoryId, inventoryId = ids?.inventoryId,
itemId = ids?.itemId, itemId = ids?.itemId,
) )
@ -30,9 +62,14 @@ class ItemDetailDialogViewModel(
initialValue = null initialValue = null
) )
fun showItemDialog(inventoryId: String?, itemId: String?) { fun showItemDialog(
characterSheetId: String,
inventoryId: String?,
itemId: String?,
) {
selectedItemId.update { selectedItemId.update {
InventoryItemId( InventoryItemId(
characterSheetId = characterSheetId,
inventoryId = inventoryId, inventoryId = inventoryId,
itemId = itemId, itemId = itemId,
) )
@ -43,7 +80,63 @@ class ItemDetailDialogViewModel(
selectedItemId.update { null } 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( private data class InventoryItemId(
val characterSheetId: String,
val inventoryId: String?, val inventoryId: String?,
val itemId: 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.Arrangement
import androidx.compose.foundation.layout.Box 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.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.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.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog 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.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
@ -66,27 +70,103 @@ data class PurseDialogUio(
val enableConfirm: StateFlow<Boolean>, 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 @Composable
fun PurseDialog( fun PurseDialog(
dialog: State<PurseDialogUio?>, dialog: State<PurseDialogUio?>,
paddings: PaddingValues = PurseDialogDefault.paddings,
spacings: DpSize = PurseDialogDefault.spacings,
onConfirm: (PurseDialogUio) -> Unit, onConfirm: (PurseDialogUio) -> Unit,
onSwapSign: (PurseDialogUio) -> Unit, onSwapSign: (PurseDialogUio) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
dialog.value?.let { LwaDialog(
Dialog( state = dialog,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
content = { onConfirm = { dialog.value?.let(onConfirm) }
PurseContent( ) { state ->
dialog = it, val focusRequester = remember { FocusRequester() }
onConfirm = onConfirm, LaunchedEffect(Unit) { focusRequester.requestFocus() }
onSwapSign = onSwapSign,
onDismissRequest = onDismissRequest, Column(
) modifier = Modifier.padding(paddingValues = paddings),
PurseDialogKeyHandler( verticalArrangement = Arrangement.spacedBy(space = spacings.height),
onSwap = { onSwapSign(it) }, 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, onSwapSign: (PurseDialogUio) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() }
Box( Box(
modifier = Modifier modifier = Modifier
@ -119,76 +198,7 @@ private fun PurseContent(
) { ) {
DecoratedBox { DecoratedBox {
Surface { 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 return false
} }
// Get the player inventory // Get the player inventory
val inventory = inventoryRepository val inventory = inventoryRepository.inventory(characterSheetId = dialog.characterSheetId)
.inventory(characterSheetId = dialog.characterSheetId)
?: return false
// compute the new purse // compute the new purse
val sign = if (dialog.add.value) 1 else -1 val sign = if (dialog.add.value) 1 else -1
val goldValue = dialog.gold.valueFlow.value.toIntOrNull() ?: 0 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.Box
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -27,6 +28,7 @@ fun <T> TooltipLayout2(
tooltip = { tooltip = {
Box( Box(
modifier = Modifier.width(width = 448.dp), modifier = Modifier.width(width = 448.dp),
contentAlignment = Alignment.TopEnd,
content = { tooltip(tips) }, content = { tooltip(tips) },
) )
}, },

View file

@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialog import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialog
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogViewModel
@ -104,11 +105,12 @@ fun CharacterDetailInventory(
characterSheetId = it, characterSheetId = it,
) )
}, },
onItem = { onItem = { item ->
blur.show() blur.show()
itemDetailDialogViewModel.showItemDialog( itemDetailDialogViewModel.showItemDialog(
inventoryId = it.inventoryId, characterSheetId = item.characterSheetId,
itemId = it.itemId, inventoryId = item.inventoryId,
itemId = item.itemId,
) )
}, },
onAddItem = { onAddItem = {
@ -145,9 +147,10 @@ fun CharacterDetailInventory(
blur.hide() blur.hide()
inventoryDialogViewModel.hideInventoryDialog() inventoryDialogViewModel.hideInventoryDialog()
}, },
onItem = { itemId -> onItem = { dialog, itemId ->
blur.show() blur.show()
itemDetailDialogViewModel.showItemDialog( itemDetailDialogViewModel.showItemDialog(
characterSheetId = dialog.characterSheetId,
inventoryId = null, inventoryId = null,
itemId = itemId, itemId = itemId,
) )
@ -159,6 +162,25 @@ fun CharacterDetailInventory(
onDismissRequest = { onDismissRequest = {
blur.hide() blur.hide()
itemDetailDialogViewModel.hideItemDialog() 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 }, key = { it.inventoryId },
) { ) {
InventoryItem( InventoryItem(
modifier = Modifier modifier = Modifier.animateItem(),
.animateItem()
.fillMaxWidth(),
item = it, item = it,
onClick = { onItem(it) }, onClick = { onItem(it) },
) )

View file

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

View file

@ -49,4 +49,15 @@ class InventoryService(
fun delete(characterSheetId: String) { fun delete(characterSheetId: String) {
inventoryStore.delete(characterSheetId = characterSheetId) 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.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.util.UUID
class InventoryStore( class InventoryStore(
private val pathProvider: PathProvider, 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 { private fun inventoryFile(id: String): File {
return File("${pathProvider.inventoryPath()}${id}.json") 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.putCharacterDamage
import com.pixelized.server.lwa.server.rest.character.putCharacterDiminished 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.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.deleteInventory
import com.pixelized.server.lwa.server.rest.inventory.getInventory import com.pixelized.server.lwa.server.rest.inventory.getInventory
import com.pixelized.server.lwa.server.rest.inventory.putInventory import com.pixelized.server.lwa.server.rest.inventory.putInventory
@ -258,6 +259,10 @@ class LocalServer {
path = "/update", path = "/update",
body = engine.putInventory() body = engine.putInventory()
) )
put(
path = "/create",
body = engine.createInventory()
)
delete( delete(
path = "/delete", path = "/delete",
body = engine.deleteInventory() body = engine.deleteInventory()

View file

@ -1,4 +1,6 @@
package com.pixelized.server.lwa.server.exception 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.")) ServerException(root = Exception("Missing '$name' parameter."))

View file

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

View file

@ -1,9 +1,9 @@
package com.pixelized.server.lwa.server.rest.character package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine 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.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception 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.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond import io.ktor.server.response.respond
@ -14,8 +14,10 @@ fun Engine.putCharacterDiminished(): suspend RoutingContext.() -> Unit {
try { try {
// get the query parameter // get the query parameter
val characterSheetId = call.queryParameters.characterSheetId val characterSheetId = call.queryParameters.characterSheetId
val diminished = call.queryParameters["diminished"]?.toIntOrNull() val diminished: Int = call.queryParameters.param(
?: throw MissingParameterException(name = "diminished") name = "diminished",
code = APIResponse.ErrorCode.Diminished,
)
// Update the character damage // Update the character damage
characterService.updateDiminished( characterService.updateDiminished(
characterSheetId = characterSheetId, 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.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception 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.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond import io.ktor.server.response.respond
@ -14,8 +15,10 @@ fun Engine.putCharacterFatigue(): suspend RoutingContext.() -> Unit {
try { try {
// get the query parameter // get the query parameter
val characterSheetId = call.queryParameters.characterSheetId val characterSheetId = call.queryParameters.characterSheetId
val fatigue = call.queryParameters["fatigue"]?.toIntOrNull() val fatigue: Int = call.queryParameters.param(
?: throw MissingParameterException(name = "fatigue") name = "fatigue",
code = APIResponse.ErrorCode.Fatigue,
)
// fetch the character sheet // fetch the character sheet
val characterSheet = characterService.character(characterSheetId) val characterSheet = characterService.character(characterSheetId)
?: error("CharacterSheet with id:$characterSheetId not found.") ?: 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 { try {
// get the query parameter // get the query parameter
val characterSheetId = call.queryParameters.characterSheetId val characterSheetId = call.queryParameters.characterSheetId
// delete the alteration. // delete the inventory.
inventoryService.delete( inventoryService.delete(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
) )

View file

@ -1,29 +1,49 @@
package com.pixelized.server.lwa.utils.extentions package com.pixelized.server.lwa.utils.extentions
import com.pixelized.server.lwa.server.exception.MissingParameterException import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.http.Parameters import io.ktor.http.Parameters
val Parameters.characterSheetId inline fun <reified T> Parameters.param(
get() = "characterSheetId".let { param -> name: String,
this[param] ?: throw MissingParameterException(name = param) 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 val Parameters.characterSheetId: String
get() = "alterationId".let { param -> get() = param(
this[param] ?: throw MissingParameterException(name = param) name = "characterSheetId",
} code = APIResponse.ErrorCode.CharacterSheetId,
)
val Parameters.itemId val Parameters.alterationId: String
get() = "itemId".let { param -> get() = param(
this[param] ?: throw MissingParameterException(name = param) name = "alterationId",
} code = APIResponse.ErrorCode.AlterationId,
)
val Parameters.create val Parameters.itemId: String
get() = "create".let { param -> get() = param(
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = param) name = "itemId",
} code = APIResponse.ErrorCode.ItemId,
)
val Parameters.active val Parameters.create: Boolean
get() = "active".let { param -> get() = param(
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = 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( message = APIResponse.error(
status = APIResponse.BAD_REQUEST, status = APIResponse.BAD_REQUEST,
message = exception.message ?: "?", message = exception.message ?: "?",
code = APIResponse.ErrorCode.AlterationName, code = exception.code,
) )
) )
} }

View file

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