Add toggable addIventoryItem action linked to the Addable tag.

This commit is contained in:
Thomas Andres Gomez 2025-04-20 12:11:27 +02:00
parent ae820f5979
commit 04825cafda
9 changed files with 180 additions and 41 deletions

View file

@ -187,6 +187,8 @@
<string name="character__inventory__filter_inventory__label">Filtrer l'inventaire</string>
<string name="character__inventory__add_to_inventory__action">Ajouter un objet</string>
<string name="character__inventory__use__action">Utiliser</string>
<string name="character__inventory__equip__action">Équiper</string>
<string name="character__inventory__unequip__action">Déséquiper</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>
<string name="character__inventory__filter_item_inventory__label">Filtrer les objets</string>
@ -194,7 +196,7 @@
<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__inventory__dialog__throw_action">Jetter</string>
<string name="character__inventory__inventory__dialog__throw_action">Jeter</string>
<string name="character__inventory__inventory__dialog__equip_action">Equiper</string>
<string name="character__inventory__inventory__dialog__consume_action">Utiliser</string>
<string name="character__inventory__description_empty__label">Cet objet n'a pas de description.</string>

View file

@ -77,6 +77,6 @@ class InventoryDialogFactory {
)
companion object {
private const val ADDABLE_TAG_ID = "META:ADDABLE"
const val ADDABLE_TAG_ID = "META:ADDABLE"
}
}

View file

@ -44,11 +44,12 @@ import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__equip__action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__consume_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__count_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__equip_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__throw_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__unequip__action
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@ -66,6 +67,7 @@ data class ItemDetailDialogUio(
// options
val countable: LwaTextFieldUio?,
val consumable: Boolean,
val equipped: Boolean,
val equipable: Boolean,
)
@ -213,9 +215,18 @@ fun ItemDetailDialog(
TextButton(
onClick = { onEquip(state) },
) {
Text(
text = stringResource(Res.string.character__inventory__inventory__dialog__equip_action),
)
AnimatedContent(
targetState = state.equipped,
) {
Text(
text = stringResource(
when (it) {
true -> Res.string.character__inventory__unequip__action
else -> Res.string.character__inventory__equip__action
}
)
)
}
}
}
if (state.consumable) {

View file

@ -11,13 +11,14 @@ import java.text.DecimalFormat
class ItemDetailDialogFactory {
private val floatChecker = Regex("""^\d*[.,]?\d*${'$'}""")
private val floatChecker = Regex("""^\d*,?\d*${'$'}""")
private val format = DecimalFormat("#.##")
suspend fun convertToDialogUio(
characterSheetId: String?,
items: Map<String, Item>,
count: Float,
equipped: Boolean,
inventoryId: String?,
itemId: String?,
): ItemDetailDialogUio? {
@ -38,6 +39,7 @@ class ItemDetailDialogFactory {
?.let { createFieldFlow(value = format.format(count)) }
?.createTextField(label = getString(Res.string.character__inventory__inventory__dialog__count)),
consumable = item.options.consumable,
equipped = equipped,
equipable = item.options.equipable,
)
}

View file

@ -52,6 +52,7 @@ class ItemDetailDialogViewModel(
characterSheetId = ids?.characterSheetId,
items = items,
count = selectedInventoryItem?.count ?: 0f,
equipped = selectedInventoryItem?.equipped ?: false,
inventoryId = ids?.inventoryId,
itemId = ids?.itemId,
)
@ -83,7 +84,7 @@ class ItemDetailDialogViewModel(
suspend fun onAddInventoryItem(
characterSheetId: String,
itemId: String,
) : Boolean {
): Boolean {
try {
// create the inventory item on the server, get the newly create id from that.
val inventoryId = inventoryRepository.createInventoryItem(
@ -143,7 +144,7 @@ class ItemDetailDialogViewModel(
suspend fun equipInventoryItem(
characterSheetId: String,
inventoryId: String?,
) : Boolean {
): Boolean {
if (inventoryId == null) return false
try {
inventoryRepository.equipInventoryItem(

View file

@ -34,6 +34,13 @@ class CharacterDetailPanelViewModel(
) : ViewModel() {
private val characterSheetPanelFlow = MutableStateFlow<CharacterSheetPanel?>(null)
private val addItemAction = characterInventoryFactory
.addItemActionFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = false,
)
val detail: StateFlow<CharacterDetailPanelUio> = characterSheetPanelFlow
.map { characterSheetPanel ->
@ -55,6 +62,7 @@ class CharacterDetailPanelViewModel(
inventory = characterInventoryFactory.convertToCharacterInventoryUioFlow(
characterSheetId = characterSheetId,
scope = viewModelScope,
addItemAction = addItemAction,
initialValue = ::emptyInventory,
),
)

View file

@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -24,9 +26,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalBlurController
@ -46,7 +52,7 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item
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.flow.StateFlow
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__add_to_inventory__action
@ -58,6 +64,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Stable
data class CharacterDetailInventoryUio(
val characterSheetId: String,
val addItemAction: StateFlow<Boolean>,
val filter: LwaTextFieldUio,
val purse: PurseUio,
val items: List<InventoryItemUio>,
@ -125,6 +132,14 @@ fun CharacterDetailInventory(
inventoryId = it.inventoryId,
)
}
},
onEquip = {
scope.launch {
itemDetailDialogViewModel.equipInventoryItem(
characterSheetId = it.characterSheetId,
inventoryId = it.inventoryId,
)
}
}
)
}
@ -248,14 +263,16 @@ private fun CharacterDetailInventoryContent(
onPurse: (String) -> Unit,
onItem: (InventoryItemUio) -> Unit,
onConsume: (InventoryItemUio) -> Unit,
onEquip: (InventoryItemUio) -> Unit,
onAddItem: (String) -> Unit,
) {
val addItemAction = inventory.addItemAction.collectAsState()
Box(
modifier = modifier,
) {
LazyColumn(
modifier = Modifier.matchParentSize(),
contentPadding = paddings + PaddingValues(bottom = 56.dp),
contentPadding = paddings.plus(addItemAction, PaddingValues(bottom = 56.dp)),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
item(
@ -309,31 +326,61 @@ private fun CharacterDetailInventoryContent(
item = it,
onClick = { onItem(it) },
onConsume = { onConsume(it) },
onEquip = { onEquip(it) },
)
}
}
Row(
AnimatedVisibility(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(paddingValues = paddings),
horizontalArrangement = Arrangement.SpaceBetween,
visible = addItemAction.value,
enter = fadeIn(),
exit = fadeOut(),
) {
Button(
colors = LwaButtonColors(),
elevation = ButtonDefaults.elevation(4.dp),
shape = CircleShape,
onClick = { onAddItem(inventory.characterSheetId) },
Row(
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.character__inventory__add_to_inventory__action),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
Button(
colors = LwaButtonColors(),
elevation = ButtonDefaults.elevation(4.dp),
shape = CircleShape,
onClick = { onAddItem(inventory.characterSheetId) },
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.character__inventory__add_to_inventory__action),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
@Stable
@Composable
fun PaddingValues.plus(
state: State<Boolean>,
other: PaddingValues,
): PaddingValues {
val direction = LocalLayoutDirection.current
val sum by remember(this, other, direction, state) {
derivedStateOf {
if (state.value) {
PaddingValues(
start = calculateStartPadding(direction) + other.calculateStartPadding(direction),
top = calculateTopPadding() + other.calculateTopPadding(),
end = calculateEndPadding(direction) + other.calculateEndPadding(direction),
bottom = calculateBottomPadding() + other.calculateBottomPadding(),
)
} else {
this
}
}
}
return sum
}

View file

@ -2,6 +2,8 @@ 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.repository.tag.TagRepository
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogFactory
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
@ -9,6 +11,7 @@ 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.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -24,11 +27,19 @@ class CharacterDetailInventoryFactory(
private val inventoryRepository: InventoryRepository,
private val itemRepository: ItemRepository,
) {
fun addItemActionFlow(): Flow<Boolean> {
return itemRepository.itemFlow().map { entry ->
entry.values.any { item ->
item.tags.any { id -> id == ADDABLE_TAG_ID }
}
}
}
suspend fun convertToCharacterInventoryUioFlow(
characterSheetId: String,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.Eagerly,
addItemAction: StateFlow<Boolean>,
initialValue: () -> CharacterDetailInventoryUio?,
): StateFlow<CharacterDetailInventoryUio?> {
val filterFlow = MutableStateFlow("")
@ -48,8 +59,9 @@ class CharacterDetailInventoryFactory(
convertToCharacterInventoryUio(
characterSheetId = characterSheetId,
filter = filterField,
purse = inventory?.purse,
inventory = inventory?.items,
addItemAction = addItemAction,
purse = inventory.purse,
inventory = inventory.items,
items = items.filterValues { it.metadata.label.unAccent().contains(filter, true) },
)
}.stateIn(
@ -62,6 +74,7 @@ class CharacterDetailInventoryFactory(
private suspend fun convertToCharacterInventoryUio(
characterSheetId: String?,
filter: LwaTextFieldUio,
addItemAction: StateFlow<Boolean>,
purse: Inventory.Purse?,
inventory: List<Inventory.Item>?,
items: Map<String, Item>,
@ -76,6 +89,7 @@ class CharacterDetailInventoryFactory(
copper = purse?.copper ?: 0,
),
filter = filter,
addItemAction = addItemAction,
items = inventory
?.mapNotNull {
val item = items[it.itemId] ?: return@mapNotNull null
@ -87,6 +101,7 @@ class CharacterDetailInventoryFactory(
count = it.count,
equipped = it.equipped,
consumable = item.options.consumable,
equipable = item.options.equipable,
tooltips = takeIf { item.metadata.description.isNotEmpty() }?.let {
InventoryItemUio.Tooltips(
label = item.metadata.label,
@ -100,4 +115,8 @@ class CharacterDetailInventoryFactory(
?: emptyList()
)
}
companion object {
private const val ADDABLE_TAG_ID = InventoryDialogFactory.ADDABLE_TAG_ID
}
}

View file

@ -1,8 +1,11 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item
import androidx.compose.animation.AnimatedContent
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.ExperimentalFoundationApi
import androidx.compose.foundation.background
@ -12,6 +15,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.calculateEndPadding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@ -23,12 +27,14 @@ import androidx.compose.material.TextButton
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
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
@ -40,6 +46,8 @@ import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.ribbon
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__equip__action
import lwacharactersheet.composeapp.generated.resources.character__inventory__unequip__action
import lwacharactersheet.composeapp.generated.resources.character__inventory__use__action
import org.jetbrains.compose.resources.stringResource
@ -52,6 +60,7 @@ data class InventoryItemUio(
val count: Float,
val equipped: Boolean,
val consumable: Boolean,
val equipable: Boolean,
val tooltips: Tooltips?,
) {
@Stable
@ -68,7 +77,10 @@ object GMCharacterPreviewDefault {
val paddings = PaddingValues(horizontal = 16.dp)
@Stable
val spacing: Dp = 4.dp
val toolTipPaddings = PaddingValues(all = 16.dp)
@Stable
val spacing: Dp = 8.dp
}
@OptIn(ExperimentalFoundationApi::class)
@ -76,11 +88,20 @@ object GMCharacterPreviewDefault {
fun InventoryItem(
modifier: Modifier = Modifier,
padding: PaddingValues = GMCharacterPreviewDefault.paddings,
toolTipPaddings: PaddingValues = GMCharacterPreviewDefault.toolTipPaddings,
spacing: Dp = GMCharacterPreviewDefault.spacing,
item: InventoryItemUio,
onClick: () -> Unit,
onConsume: () -> Unit,
onEquip: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val toolTop = remember(toolTipPaddings) { toolTipPaddings.calculateTopPadding() }
val toolEnd = remember(toolTipPaddings, layoutDirection) {
toolTipPaddings.calculateEndPadding(layoutDirection)
}
val end = remember(padding, layoutDirection) { padding.calculateEndPadding(layoutDirection) }
TooltipLayout2(
modifier = modifier,
delayMillis = 500,
@ -89,14 +110,14 @@ fun InventoryItem(
DecoratedBox {
Surface {
Box(
modifier = Modifier.padding(all = 16.dp)
modifier = Modifier.padding(paddingValues = toolTipPaddings)
) {
takeIf { tooltips.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage(
modifier = Modifier
.size(96.dp)
.size(size = 96.dp)
.align(alignment = Alignment.TopEnd)
.offset(x = 8.dp, y = (-8).dp),
.offset(x = toolEnd, y = -toolTop),
colorFilter = rememberSaturationFilter(),
model = tooltips.image,
contentScale = ContentScale.Crop,
@ -107,7 +128,7 @@ fun InventoryItem(
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
verticalArrangement = Arrangement.spacedBy(space = spacing)
) {
Text(
style = MaterialTheme.typography.body2,
@ -144,10 +165,11 @@ fun InventoryItem(
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.weight(weight = 1f),
horizontalArrangement = Arrangement.spacedBy(space = spacing),
) {
Text(
modifier = Modifier.alignByBaseline(),
modifier = Modifier.alignByBaseline().weight(weight = 1f, fill = false),
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
@ -157,7 +179,11 @@ fun InventoryItem(
AnimatedContent(
modifier = Modifier.alignByBaseline(),
targetState = item.count,
transitionSpec = { fadeIn() togetherWith fadeOut() },
transitionSpec = {
val enter = fadeIn() + slideInHorizontally { 16 }
val exit = fadeOut() + slideOutHorizontally { -16 }
enter togetherWith exit using SizeTransform(clip = false)
},
) {
when (it) {
0f, 1f -> Unit
@ -170,11 +196,34 @@ fun InventoryItem(
}
}
}
if (item.consumable) {
TextButton(
onClick = onConsume,
) {
Text(text = stringResource(Res.string.character__inventory__use__action))
Row(
modifier = Modifier.offset(x = end - spacing),
horizontalArrangement = Arrangement.spacedBy(space = spacing),
) {
if (item.consumable) {
TextButton(
onClick = onConsume,
) {
Text(text = stringResource(Res.string.character__inventory__use__action))
}
}
if (item.equipable) {
TextButton(
onClick = onEquip,
) {
AnimatedContent(
targetState = item.equipped,
) {
Text(
text = stringResource(
when (it) {
true -> Res.string.character__inventory__unequip__action
else -> Res.string.character__inventory__equip__action
}
)
)
}
}
}
}
}