Add Addable InventoryItem feature & quantity change.
This commit is contained in:
parent
48074f3d13
commit
9fce3f1cb8
28 changed files with 785 additions and 343 deletions
|
|
@ -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 d’un 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
content = {
|
||||
onConfirm = onDismissRequest,
|
||||
) {
|
||||
InventoryDialogContent(
|
||||
dialog = it,
|
||||
paddings = paddings,
|
||||
spacing = spacing,
|
||||
onDismissRequest = onDismissRequest,
|
||||
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,24 +134,9 @@ 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()
|
||||
|
|
@ -221,14 +205,11 @@ private fun InventoryDialogContent(
|
|||
InventoryDialogItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
item = item,
|
||||
onItem = onItem,
|
||||
onItem = { onItem(dialog, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
|
|||
|
|
@ -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,106 +31,80 @@ 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(
|
||||
LwaDialog(
|
||||
state = dialog,
|
||||
onDismissRequest = onDismissRequest,
|
||||
content = {
|
||||
ItemDetailDialogContent(
|
||||
dialog = it,
|
||||
paddings = paddings,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemDetailDialogContent(
|
||||
modifier: Modifier = Modifier,
|
||||
paddings: PaddingValues,
|
||||
dialog: ItemDetailDialogUio,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
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() }
|
||||
|
||||
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)
|
||||
) {
|
||||
takeIf { dialog.image?.isNotEmpty() == true }?.let {
|
||||
takeIf { state.image?.isNotEmpty() == true }?.let {
|
||||
DesaturatedAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(64.dp * 2)
|
||||
.align(alignment = Alignment.TopEnd),
|
||||
colorFilter = rememberSaturationFilter(),
|
||||
model = dialog.image,
|
||||
model = state.image,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopCenter,
|
||||
filterQuality = FilterQuality.High,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
|
||||
modifier = Modifier.padding(paddingValues = paddings),
|
||||
verticalArrangement = Arrangement.spacedBy(space = spacings)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -141,7 +117,7 @@ private fun ItemDetailDialogContent(
|
|||
fontWeight = FontWeight.Bold,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
text = dialog.label,
|
||||
text = state.label,
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier.offset(x = end, y = -top),
|
||||
|
|
@ -154,13 +130,68 @@ private fun ItemDetailDialogContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
LwaDialog(
|
||||
state = dialog,
|
||||
onDismissRequest = onDismissRequest,
|
||||
content = {
|
||||
PurseContent(
|
||||
dialog = it,
|
||||
onConfirm = onConfirm,
|
||||
onSwapSign = onSwapSign,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
PurseDialogKeyHandler(
|
||||
onSwap = { onSwapSign(it) },
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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."))
|
||||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ data class APIResponse<T>(
|
|||
ItemId,
|
||||
ItemName,
|
||||
CharacterSheetId,
|
||||
Create,
|
||||
Active,
|
||||
Damage,
|
||||
Fatigue,
|
||||
Diminished,
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue