Add client inventory sheet management.

This commit is contained in:
Thomas Andres Gomez 2025-04-17 14:29:22 +02:00
parent 8982bab22d
commit 05a376aea8
10 changed files with 325 additions and 118 deletions

View file

@ -184,6 +184,8 @@
<string name="character_sheet__delete_dialog__title">Supprimer la feuille de personnage</string>
<string name="character_sheet__delete_dialog__description">Êtes-vous sûr de vouloir supprimer "%1$s" ?</string>
<string name="character__inventory__add_to_inventory__action">Ajouter un objet</string>
<string name="character__inventory__filter_inventory__label">Filtrer l'inventaire</string>
<string name="character__inventory__add_to_purse__title">Ajouter à la bourse</string>
<string name="character__inventory__remove_from_purse__title">Retirer de la bourse</string>

View file

@ -1,29 +1,26 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
@ -31,45 +28,57 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialog
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryItem
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryItemUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryPurse
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.PurseUio
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
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.plus
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_copper_32px
import lwacharactersheet.composeapp.generated.resources.ic_gold_32px
import lwacharactersheet.composeapp.generated.resources.ic_silver_32px
import lwacharactersheet.composeapp.generated.resources.character__inventory__add_to_inventory__action
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Stable
data class CharacterDetailInventoryUio(
val characterSheetId: String,
val filter: LwaTextFieldUio,
val purse: PurseUio,
val items: List<InventoryItemUio>,
) {
@Stable
data class PurseUio(
val gold: Int,
val silver: Int,
val copper: Int,
)
}
)
@Stable
object CharacterDetailInventoryDefault {
val padding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
@Stable
val padding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 8.dp,
bottom = 8.dp,
)
@Stable
val spacing: Dp = 8.dp
}
@Composable
fun CharacterDetailInventory(
modifier: Modifier = Modifier,
paddings: PaddingValues = CharacterDetailInventoryDefault.padding,
spacing: Dp = CharacterDetailInventoryDefault.spacing,
purseViewModel: PurseDialogViewModel = koinViewModel(),
inventory: State<CharacterDetailInventoryUio?>,
) {
@ -82,6 +91,7 @@ fun CharacterDetailInventory(
else -> CharacterDetailInventoryContent(
modifier = modifier,
paddings = paddings,
spacing = spacing,
inventory = unWrap,
onPurse = {
blur.show()
@ -118,119 +128,95 @@ fun CharacterDetailInventory(
private fun CharacterDetailInventoryContent(
modifier: Modifier = Modifier,
paddings: PaddingValues,
spacing: Dp,
inventory: CharacterDetailInventoryUio,
onPurse: (String) -> Unit,
) {
Column(
Box(
modifier = modifier,
) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = paddings,
modifier = Modifier.matchParentSize(),
contentPadding = paddings + PaddingValues(bottom = 56.dp),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
item(
key = "purse",
) {
Row(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(space = spacing),
) {
InventoryPurse(
modifier = Modifier.weight(weight = 1f),
purse = inventory.purse,
onPurse = { onPurse(inventory.characterSheetId) },
)
LwaTextField(
modifier = Modifier.weight(weight = 1f),
colors = LwaTextFieldColors(
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base2dp,
),
field = inventory.filter,
trailingIcon = {
val value = inventory.filter.valueFlow.collectAsState()
AnimatedVisibility(
visible = value.value.isNotBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = { inventory.filter.onValueChange.invoke("") },
) {
Icon(
painter = painterResource(Res.drawable.ic_cancel_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
}
)
}
}
items(
items = inventory.items,
key = { it.inventoryId },
) {
InventoryItem(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.animateItem()
.fillMaxWidth(),
item = it,
onClick = { },
)
}
}
Purse(
modifier = Modifier.fillMaxWidth(),
paddings = paddings,
purse = inventory.purse,
onPurse = { onPurse(inventory.characterSheetId) },
)
}
}
@Composable
private fun Purse(
modifier: Modifier = Modifier,
paddings: PaddingValues,
purse: CharacterDetailInventoryUio.PurseUio,
onPurse: () -> Unit,
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.then(other = modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
) {
Row(
modifier = Modifier
.clickable { onPurse() }
.align(alignment = Alignment.BottomEnd)
.padding(paddingValues = paddings),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_gold_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
Button(
colors = LwaButtonColors(),
elevation = ButtonDefaults.elevation(4.dp),
shape = CircleShape,
onClick = { },
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.character__inventory__add_to_inventory__action),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
AnimatedContent(
targetState = purse.gold,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_silver_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.silver,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_copper_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.copper,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
}
}
}
@Composable
@Stable
private fun coinTransitionSpec(): AnimatedContentTransitionScope<Int>.() -> ContentTransform = {
val enter = fadeIn() + slideInVertically { -16 }
val exit = fadeOut() + slideOutVertically { 16 }
enter togetherWith exit using SizeTransform(clip = false)
}

View file

@ -2,35 +2,55 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory
import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.InventoryItemUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item.PurseUio
import com.pixelized.desktop.lwa.utils.extention.unAccent
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.item.Item
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__filter_inventory__label
import org.jetbrains.compose.resources.getString
import java.text.Collator
class CharacterDetailInventoryFactory(
private val inventoryRepository: InventoryRepository,
private val itemRepository: ItemRepository,
) {
fun convertToCharacterInventoryUioFlow(
suspend fun convertToCharacterInventoryUioFlow(
characterSheetId: String,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.Eagerly,
initialValue: () -> CharacterDetailInventoryUio?,
): StateFlow<CharacterDetailInventoryUio?> {
val filterFlow = MutableStateFlow("")
val filterField = LwaTextFieldUio(
enable = true,
isError = MutableStateFlow(false),
valueFlow = filterFlow,
label = getString(Res.string.character__inventory__filter_inventory__label),
placeHolder = null,
onValueChange = { filterFlow.value = it },
)
return combine(
inventoryRepository.inventoryFlow(characterSheetId = characterSheetId),
itemRepository.itemFlow,
) { inventory, items ->
filterFlow.map { it.unAccent() },
) { inventory, items, filter ->
convertToCharacterInventoryUio(
characterSheetId = characterSheetId,
filter = filterField,
purse = inventory?.purse,
inventory = inventory?.items,
items = items,
items = items.filterValues { it.metadata.name.unAccent().contains(filter, true) },
)
}.stateIn(
scope = scope,
@ -41,6 +61,7 @@ class CharacterDetailInventoryFactory(
private suspend fun convertToCharacterInventoryUio(
characterSheetId: String?,
filter: LwaTextFieldUio,
purse: Inventory.Purse?,
inventory: List<Inventory.Item>?,
items: Map<String, Item>,
@ -49,19 +70,22 @@ class CharacterDetailInventoryFactory(
return CharacterDetailInventoryUio(
characterSheetId = characterSheetId,
purse = CharacterDetailInventoryUio.PurseUio(
purse = PurseUio(
gold = purse?.gold ?: 0,
silver = purse?.silver ?: 0,
copper = purse?.copper ?: 0,
),
filter = filter,
items = inventory
?.mapNotNull {
val label = items[it.itemId]?.metadata?.name ?: return@mapNotNull null
InventoryItemUio(
inventoryId = it.inventoryId,
label = label,
equipped = it.equipped,
)
}
?.sortedWith(compareBy(Collator.getInstance()) { it.label })
?: emptyList()
)
}

View file

@ -12,13 +12,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.ribbon
@Stable
data class InventoryItemUio(
val inventoryId: String,
val label: String,
val equipped: Boolean,
)
@Stable
@ -36,9 +39,15 @@ fun InventoryItem(
) {
Row(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.gameMaster)
.clip(shape = MaterialTheme.lwa.shapes.item)
.clickable(onClick = onClick)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.ribbon(
color = when (item.equipped) {
true -> MaterialTheme.lwa.colorScheme.base.primary
else -> Color.Transparent
}
)
.minimumInteractiveComponentSize()
.padding(paddingValues = padding)
.then(other = modifier),

View file

@ -0,0 +1,130 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_copper_32px
import lwacharactersheet.composeapp.generated.resources.ic_gold_32px
import lwacharactersheet.composeapp.generated.resources.ic_silver_32px
import org.jetbrains.compose.resources.painterResource
@Stable
data class PurseUio(
val gold: Int,
val silver: Int,
val copper: Int,
)
@Stable
object InventoryPurseDefault {
@Stable
val paddings = PaddingValues(horizontal = 0.dp, vertical = 4.dp)
}
@Composable
fun InventoryPurse(
modifier: Modifier = Modifier,
paddings: PaddingValues = InventoryPurseDefault.paddings,
purse: PurseUio,
onPurse: () -> Unit,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Row(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.item)
.clickable { onPurse() }
.padding(paddingValues = paddings),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_gold_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.gold,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_silver_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.silver,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
Row(verticalAlignment = Alignment.Bottom) {
Image(
painter = painterResource(Res.drawable.ic_copper_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
AnimatedContent(
targetState = purse.copper,
transitionSpec = coinTransitionSpec(),
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
text = "$it",
)
}
}
}
}
}
@Composable
@Stable
private fun coinTransitionSpec(): AnimatedContentTransitionScope<Int>.() -> ContentTransform = {
val enter = fadeIn() + slideInHorizontally { -16 }
val exit = fadeOut() + slideOutHorizontally { 16 }
enter togetherWith exit using SizeTransform(clip = false)
}

View file

@ -0,0 +1,29 @@
package com.pixelized.desktop.lwa.utils.extention
import androidx.compose.animation.animateColorAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
fun Modifier.ribbon(
width: Dp = 4.dp,
color: Color,
): Modifier = composed {
val animatedColor = animateColorAsState(
targetValue = color,
)
return@composed drawWithContent {
drawContent()
drawRect(
color = animatedColor.value,
size = Size(
width = width.toPx(),
height = size.height,
)
)
}
}

View file

@ -0,0 +1,23 @@
package com.pixelized.desktop.lwa.utils.extention
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLayoutDirection
@Stable
@Composable
operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
val direction = LocalLayoutDirection.current
return remember(this, other, direction) {
PaddingValues(
start = calculateStartPadding(direction) + other.calculateStartPadding(direction),
top = calculateTopPadding() + other.calculateTopPadding(),
end = calculateEndPadding(direction) + other.calculateEndPadding(direction),
bottom = calculateBottomPadding() + other.calculateBottomPadding(),
)
}
}

View file

@ -15,6 +15,7 @@ data class Inventory(
val inventoryId: String,
val itemId: String,
val count: Int,
val equipped: Boolean,
)
companion object {

View file

@ -21,5 +21,6 @@ data class InventoryJsonV1(
val inventoryId: String,
val itemId: String,
val count: Int,
val equipped: Boolean?,
)
}

View file

@ -18,6 +18,7 @@ class InventoryJsonFactoryV1 {
inventoryId = it.inventoryId,
itemId = it.itemId,
count = it.count,
equipped = it.equipped ?: false,
)
},
)
@ -36,6 +37,7 @@ class InventoryJsonFactoryV1 {
inventoryId = it.inventoryId,
itemId = it.itemId,
count = it.count,
equipped = it.equipped,
)
},
)