Add AddItem dialog to the inventory screen.

This commit is contained in:
Thomas Andres Gomez 2025-04-18 16:00:35 +02:00
parent c94c820efb
commit 48074f3d13
17 changed files with 704 additions and 84 deletions

View file

@ -188,6 +188,9 @@
<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>
<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__description_empty__label">Cet objet n'a pas de description.</string>
<string name="tooltip__characteristics__characteristics">Les caractéristiques constituent les aptitudes innées dun personnage comme son intelligence, sa force, son charisme, etc. Elles ne sont pas acquises, mais peuvent être parfois augmentées par un entraînement ou une utilisation réussie. Les caractéristiques des humains normaux varient de 2 (niveau extrêmement bas) à 20 (maximum du potentiel humain), avec une moyenne de 10 ou 11. Plus une caractéristique est élevée plus le personnage est puissant dans cette aptitude.\nÀ la création de votre personnage, répartissez les valeurs suivantes dans les différentes caractéristiques : 15, 15, 13, 11, 10, 9 et 7.</string>
<string name="tooltip__characteristics__strength">La Force représente essentiellement la puissance musculaire du personnage. Elle ne décrit pas nécessairement la masse musculaire brute, mais lefficacité avec laquelle le personnage exerce ses muscles pour accomplir des actions physiques pénibles.\n\n- Bonus aux dégats\n- Réflexe\n- Athlétisme\n- Lancer\n- Saisie</string>

View file

@ -5,7 +5,7 @@ import com.pixelized.shared.lwa.model.item.Item
class ItemRepository(
private val itemStore: ItemStore,
) {
val itemFlow get() = itemStore.items
fun itemFlow() = itemStore.items
suspend fun updateItemFlow() {
itemStore.updateItemFlow()
@ -14,7 +14,7 @@ class ItemRepository(
fun item(
itemId: String?,
): Item? {
return itemFlow.value[itemId]
return itemStore.items.value[itemId]
}
@Throws

View file

@ -1,8 +1,308 @@
package com.pixelized.desktop.lwa.ui.composable.character.inventory
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
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
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
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
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.minimumInteractiveComponentSize
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
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
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.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.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
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class InventoryDialogUio(
val characterSheetId: String,
val filter: LwaTextFieldUio,
val items: List<Item>,
) {
@Stable
data class Item(
val itemId: String,
val label: String,
val tooltips: ToolTip?,
) {
@Stable
data class ToolTip(
val label: String,
val description: String,
val image: String?,
)
}
}
@Stable
object InventoryDialogDefault {
@Stable
val paddings = PaddingValues(all = 16.dp)
@Stable
val spacings = 8.dp
}
@Stable
object InventoryDialogItemDefault {
@Stable
val paddings = PaddingValues(horizontal = 16.dp)
@Stable
val spacings = 8.dp
}
@Composable
fun InventoryDialog() {
fun InventoryDialog(
dialog: State<InventoryDialogUio?>,
paddings: PaddingValues = InventoryDialogDefault.paddings,
onDismissRequest: () -> Unit,
onItem: (String) -> Unit,
) {
dialog.value?.let {
Dialog(
onDismissRequest = onDismissRequest,
content = {
InventoryDialogContent(
dialog = it,
paddings = paddings,
onDismissRequest = onDismissRequest,
onItem = onItem,
)
}
)
}
}
@Composable
private fun InventoryDialogContent(
dialog: InventoryDialogUio,
paddings: PaddingValues = InventoryDialogDefault.paddings,
spacing: Dp = InventoryDialogDefault.spacings,
onDismissRequest: () -> Unit,
onItem: (String) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val start = remember(layoutDirection) { paddings.calculateStartPadding(layoutDirection) }
val end = remember(layoutDirection) { paddings.calculateEndPadding(layoutDirection) }
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,
) {
DecoratedBox {
Surface {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = top, start = start, bottom = spacing, end = end),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.weight(weight = 1f),
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character__inventory__inventory__dialog__title),
)
IconButton(
modifier = Modifier.offset(x = end, y = -top),
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
LwaTextField(
colors = LwaTextFieldColors(backgroundColor = Color.Transparent),
modifier = Modifier.fillMaxWidth(),
field = dialog.filter,
trailingIcon = {
val value = dialog.filter.valueFlow.collectAsState()
AnimatedVisibility(
visible = value.value.isNotBlank(),
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = { dialog.filter.onValueChange.invoke("") },
) {
Icon(
painter = painterResource(Res.drawable.ic_cancel_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
}
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 1f),
contentPadding = PaddingValues(
start = start,
top = spacing,
end = end,
bottom = bottom
),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = dialog.items,
key = { it.itemId },
) { item ->
InventoryDialogItem(
modifier = Modifier.animateItem(),
item = item,
onItem = onItem,
)
}
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun InventoryDialogItem(
modifier: Modifier = Modifier,
paddings: PaddingValues = InventoryDialogItemDefault.paddings,
spacings: Dp = InventoryDialogItemDefault.spacings,
item: InventoryDialogUio.Item,
onItem: (String) -> Unit,
) {
TooltipLayout2(
modifier = modifier,
delayMillis = 500,
tips = item.tooltips,
tooltip = { tooltips ->
DecoratedBox {
Surface {
Box(
modifier = Modifier.padding(all = 16.dp)
) {
takeIf { tooltips.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage(
modifier = Modifier
.size(96.dp)
.align(alignment = Alignment.TopEnd)
.offset(x = 8.dp, y = (-8).dp),
colorFilter = rememberSaturationFilter(),
model = tooltips.image,
contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
Column(
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
text = tooltips.label,
)
Text(
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Light,
text = tooltips.description,
)
}
}
}
}
},
content = {
Row(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.item)
.clickable(onClick = { onItem(item.itemId) })
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.minimumInteractiveComponentSize()
.padding(paddingValues = paddings)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = spacings),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = item.label,
)
}
},
)
}

View file

@ -1,5 +1,82 @@
package com.pixelized.desktop.lwa.ui.composable.character.inventory
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.utils.extention.unAccent
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__filter_item_inventory__label
import org.jetbrains.compose.resources.getString
class InventoryDialogFactory {
fun filterItems(
items: Collection<Item>,
filter: String,
setting: Settings,
): Collection<Item> {
return if (setting.isGameMaster == true) {
items.filter {
it.metadata.label.unAccent().contains(other = filter, ignoreCase = true)
}
} else {
items.filter {
it.tags.contains(ADDABLE_TAG_ID)
}.filter {
it.metadata.label.unAccent().contains(other = filter, ignoreCase = true)
}
}
}
suspend fun convertToDialogUio(
items: Collection<Item>,
filterFlow: Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>>,
characterSheetId: String?,
): InventoryDialogUio? {
if (characterSheetId == null) return null
return InventoryDialogUio(
characterSheetId = characterSheetId,
filter = filterFlow.createTextField(
label = getString(Res.string.character__inventory__filter_item_inventory__label),
),
items = items.map { item ->
InventoryDialogUio.Item(
itemId = item.id,
label = item.metadata.label,
tooltips = takeIf { item.metadata.description.isNotEmpty() }?.let {
InventoryDialogUio.Item.ToolTip(
label = item.metadata.label,
description = item.metadata.description,
image = item.metadata.image,
)
},
)
}
)
}
fun createTextFieldFlow(
value: String = "",
error: Boolean = false,
): Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>> {
return MutableStateFlow(value) to MutableStateFlow(error)
}
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 = { first.value = it },
)
companion object {
private const val ADDABLE_TAG_ID = "META:ADDABLE"
}
}

View file

@ -1,7 +1,52 @@
package com.pixelized.desktop.lwa.ui.composable.character.inventory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class InventoryDialogViewModel: ViewModel() {
class InventoryDialogViewModel(
itemRepository: ItemRepository,
settingRepository: SettingsRepository,
factory: InventoryDialogFactory,
) : ViewModel() {
private val selectedCharacterSheetId = MutableStateFlow<String?>(null)
private val filterFlow = factory.createTextFieldFlow()
val inventoryDialog = combine(
itemRepository.itemFlow().map { it.values },
settingRepository.settingsFlow(),
filterFlow.first,
selectedCharacterSheetId,
) { items, settings, filter, characterSheetId ->
factory.convertToDialogUio(
items = factory.filterItems(
items = items,
filter = filter,
setting = settings,
),
filterFlow = filterFlow,
characterSheetId = characterSheetId,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null
)
fun showInventoryDialog(characterSheetId: String?) {
filterFlow.first.update { "" }
selectedCharacterSheetId.update { characterSheetId }
}
fun hideInventoryDialog() {
selectedCharacterSheetId.update { null }
}
}

View file

@ -6,9 +6,15 @@ 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.calculateEndPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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
@ -20,16 +26,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
import androidx.compose.ui.window.Dialog
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.image.DesaturatedAsyncImage
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.ic_close_24dp
import org.jetbrains.compose.resources.painterResource
@Stable
data class ItemDetailDialogUio(
val inventoryId: String?,
val itemId: String,
val label: String,
val description: String,
@ -76,6 +89,10 @@ private fun ItemDetailDialogContent(
dialog: ItemDetailDialogUio,
onDismissRequest: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val end = remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) }
val top = remember(paddings) { paddings.calculateTopPadding() }
Box(
modifier = Modifier
.clickable(
@ -96,26 +113,47 @@ private fun ItemDetailDialogContent(
Box(
modifier = Modifier.padding(paddingValues = paddings)
) {
DesaturatedAsyncImage(
modifier = Modifier
.size(64.dp * 2)
.align(alignment = Alignment.TopEnd),
colorFilter = rememberSaturationFilter(),
model = dialog.image,
contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High,
contentDescription = null,
)
takeIf { dialog.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage(
modifier = Modifier
.size(64.dp * 2)
.align(alignment = Alignment.TopEnd),
colorFilter = rememberSaturationFilter(),
model = dialog.image,
contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
text = dialog.label,
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.weight(weight = 1f),
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = dialog.label,
)
IconButton(
modifier = Modifier.offset(x = end, y = -top),
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
Text(
style = MaterialTheme.typography.body1,
text = dialog.description,

View file

@ -1,19 +1,26 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
import com.pixelized.shared.lwa.model.item.Item
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__description_empty__label
import org.jetbrains.compose.resources.getString
class ItemDetailDialogFactory {
fun convertToDialogUio(
suspend fun convertToDialogUio(
items: Map<String, Item>,
inventoryId: String?,
itemId: String?,
): ItemDetailDialogUio? {
val item = itemId.let(items::get) ?: return null
return ItemDetailDialogUio(
inventoryId = inventoryId,
itemId = item.id,
label = item.metadata.label,
description = item.metadata.description,
description = item.metadata.description.ifBlank {
getString(Res.string.character__inventory__description_empty__label)
},
image = item.metadata.image,
option = ItemDetailDialogUio.OptionUio(
equipable = item.options.equipable,

View file

@ -14,21 +14,37 @@ class ItemDetailDialogViewModel(
factory: ItemDetailDialogFactory,
) : ViewModel() {
private val selectedItemId = MutableStateFlow<String?>(null)
private val selectedItemId = MutableStateFlow<InventoryItemId?>(null)
val itemDialog = combine(
itemRepository.itemFlow, selectedItemId,
transform = { items, itemId -> factory.convertToDialogUio(items, itemId) }
itemRepository.itemFlow(), selectedItemId,
transform = { items, ids ->
factory.convertToDialogUio(
items = items,
inventoryId = ids?.inventoryId,
itemId = ids?.itemId,
)
}
).stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null
)
fun showItemDialog(itemId: String?) {
selectedItemId.update { itemId }
fun showItemDialog(inventoryId: String?, itemId: String?) {
selectedItemId.update {
InventoryItemId(
inventoryId = inventoryId,
itemId = itemId,
)
}
}
fun hideItemDialog() {
selectedItemId.update { null }
}
private data class InventoryItemId(
val inventoryId: String?,
val itemId: String?,
)
}

View file

@ -0,0 +1,43 @@
package com.pixelized.desktop.lwa.ui.composable.tooltip
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipArea
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.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun <T> TooltipLayout2(
modifier: Modifier = Modifier,
delayMillis: Int = 1000,
tooltipPlacement: TooltipPlacement = TooltipPlacement.CursorPoint(DpOffset(0.dp, 16.dp)),
tips: T? = null,
tooltip: (@Composable (tips: T) -> Unit)? = null,
content: @Composable () -> Unit,
) {
if (tips != null && tooltip != null) {
TooltipArea(
modifier = modifier,
tooltip = {
Box(
modifier = Modifier.width(width = 448.dp),
content = { tooltip(tips) },
)
},
content = content,
delayMillis = delayMillis,
tooltipPlacement = tooltipPlacement,
)
} else {
Box(
modifier = modifier,
content = { content() },
)
}
}

View file

@ -30,6 +30,8 @@ import androidx.compose.ui.Modifier
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.inventory.InventoryDialog
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialog
import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialog
@ -81,6 +83,7 @@ fun CharacterDetailInventory(
paddings: PaddingValues = CharacterDetailInventoryDefault.padding,
spacing: Dp = CharacterDetailInventoryDefault.spacing,
purseViewModel: PurseDialogViewModel = koinViewModel(),
inventoryDialogViewModel: InventoryDialogViewModel = koinViewModel(),
itemDetailDialogViewModel: ItemDetailDialogViewModel = koinViewModel(),
inventory: State<CharacterDetailInventoryUio?>,
) {
@ -97,11 +100,22 @@ fun CharacterDetailInventory(
inventory = unWrap,
onPurse = {
blur.show()
purseViewModel.showPurseDialog(characterSheetId = it)
purseViewModel.showPurseDialog(
characterSheetId = it,
)
},
onItem = {
blur.show()
itemDetailDialogViewModel.showItemDialog(itemId = it.itemId)
itemDetailDialogViewModel.showItemDialog(
inventoryId = it.inventoryId,
itemId = it.itemId,
)
},
onAddItem = {
blur.show()
inventoryDialogViewModel.showInventoryDialog(
characterSheetId = it,
)
}
)
}
@ -125,6 +139,21 @@ fun CharacterDetailInventory(
}
)
InventoryDialog(
dialog = inventoryDialogViewModel.inventoryDialog.collectAsState(),
onDismissRequest = {
blur.hide()
inventoryDialogViewModel.hideInventoryDialog()
},
onItem = { itemId ->
blur.show()
itemDetailDialogViewModel.showItemDialog(
inventoryId = null,
itemId = itemId,
)
},
)
ItemDetailDialog(
dialog = itemDetailDialogViewModel.itemDialog.collectAsState(),
onDismissRequest = {
@ -146,6 +175,7 @@ private fun CharacterDetailInventoryContent(
inventory: CharacterDetailInventoryUio,
onPurse: (String) -> Unit,
onItem: (InventoryItemUio) -> Unit,
onAddItem: (String) -> Unit,
) {
Box(
modifier = modifier,
@ -221,7 +251,7 @@ private fun CharacterDetailInventoryContent(
colors = LwaButtonColors(),
elevation = ButtonDefaults.elevation(4.dp),
shape = CircleShape,
onClick = { },
onClick = { onAddItem(inventory.characterSheetId) },
) {
Text(
modifier = Modifier.padding(end = 4.dp),

View file

@ -42,7 +42,7 @@ class CharacterDetailInventoryFactory(
)
return combine(
inventoryRepository.inventoryFlow(characterSheetId = characterSheetId),
itemRepository.itemFlow,
itemRepository.itemFlow(),
filterFlow.map { it.unAccent() },
) { inventory, items, filter ->
convertToCharacterInventoryUio(
@ -85,6 +85,13 @@ class CharacterDetailInventoryFactory(
label = item.metadata.label,
count = it.count,
equipped = it.equipped,
tooltips = takeIf { item.metadata.description.isNotEmpty() }?.let {
InventoryItemUio.Tooltips(
label = item.metadata.label,
description = item.metadata.description,
image = item.metadata.image,
)
}
)
}
?.sortedWith(compareBy(Collator.getInstance()) { it.label })

View file

@ -1,20 +1,22 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
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.ExperimentalFoundationApi
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
@ -23,21 +25,35 @@ 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.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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.tooltip.TooltipLayout2
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.ribbon
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
@Stable
data class InventoryItemUio(
val inventoryId: String,
val itemId: String,
val label: String,
val count: Int,
val count: Float,
val equipped: Boolean,
)
val tooltips: Tooltips?,
) {
@Stable
data class Tooltips(
val label: String,
val description: String,
val image: String?,
)
}
@Stable
object GMCharacterPreviewDefault {
@ -48,6 +64,7 @@ object GMCharacterPreviewDefault {
val spacing: Dp = 4.dp
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun InventoryItem(
modifier: Modifier = Modifier,
@ -56,53 +73,90 @@ fun InventoryItem(
item: InventoryItemUio,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.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),
horizontalArrangement = Arrangement.spacedBy(space = spacing),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = item.label,
)
AnimatedVisibility(
visible = item.count > 1,
enter = fadeIn(),
exit = fadeOut(),
) {
AnimatedContent(
targetState = item.count,
transitionSpec = {
val prod = if (initialState < targetState) 1 else -1
val enter = fadeIn() + slideInVertically { -8 * prod }
val exit = fadeOut() + slideOutVertically { 8 * prod }
enter togetherWith exit using SizeTransform(clip = false)
TooltipLayout2(
delayMillis = 500,
tips = item.tooltips,
tooltip = { tooltips ->
DecoratedBox {
Surface {
Box(
modifier = Modifier.padding(all = 16.dp)
) {
takeIf { tooltips.image?.isNotEmpty() == true }?.let {
DesaturatedAsyncImage(
modifier = Modifier
.size(96.dp)
.align(alignment = Alignment.TopEnd)
.offset(x = 8.dp, y = (-8).dp),
colorFilter = rememberSaturationFilter(),
model = tooltips.image,
contentScale = ContentScale.Crop,
alignment = Alignment.TopCenter,
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
text = tooltips.label,
)
Text(
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Light,
text = tooltips.description,
)
}
}
}
}
},
content = {
Row(
modifier = Modifier
.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),
horizontalArrangement = Arrangement.spacedBy(space = spacing),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "x${it}",
text = item.label,
)
AnimatedContent(
modifier = Modifier.alignByBaseline(),
targetState = item.count,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) {
when (it) {
0f, 1f -> Unit
else -> Text(
style = MaterialTheme.lwa.typography.base.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "x${it}",
)
}
}
}
}
}
},
)
}

View file

@ -60,7 +60,7 @@ class GMItemViewModel(
)
val items: StateFlow<List<GMItemUio>> = combine(
itemRepository.itemFlow,
itemRepository.itemFlow(),
tagRepository.itemsTagFlow(),
filter.valueFlow.map { it.unAccent() },
selectedTagId,

View file

@ -26,8 +26,8 @@ class CharacterSheetJsonFactory(
val json = CharacterSheetJsonV1(
id = sheet.id,
name = sheet.name,
portrait = sheet.portrait,
thumbnail = sheet.thumbnail,
portrait = sheet.portrait?.takeIf { it.isNotBlank() },
thumbnail = sheet.thumbnail?.takeIf { it.isNotBlank() },
level = sheet.level,
shouldLevelUp = sheet.shouldLevelUp,
strength = sheet.strength,

View file

@ -14,7 +14,7 @@ data class Inventory(
data class Item(
val inventoryId: String,
val itemId: String,
val count: Int,
val count: Float,
val equipped: Boolean,
)

View file

@ -20,7 +20,7 @@ data class InventoryJsonV1(
data class ItemJson(
val inventoryId: String,
val itemId: String,
val count: Int,
val count: Float,
val equipped: Boolean?,
)
}

View file

@ -30,8 +30,8 @@ class ItemJsonFactoryV1 {
metadata = ItemJsonV1.ItemMetadataJsonV1(
name = item.metadata.label,
description = item.metadata.description,
image = item.metadata.image,
thumbnail = item.metadata.thumbnail,
image = item.metadata.image?.takeIf { it.isNotBlank() },
thumbnail = item.metadata.thumbnail?.takeIf { it.isNotBlank() },
),
options = ItemJsonV1.ItemOptionJsonV1(
stackable = item.options.stackable,