Add AddItem dialog to the inventory screen.
This commit is contained in:
parent
c94c820efb
commit
48074f3d13
17 changed files with 704 additions and 84 deletions
|
|
@ -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 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>
|
||||
<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 l’efficacité 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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ class GMItemViewModel(
|
|||
)
|
||||
|
||||
val items: StateFlow<List<GMItemUio>> = combine(
|
||||
itemRepository.itemFlow,
|
||||
itemRepository.itemFlow(),
|
||||
tagRepository.itemsTagFlow(),
|
||||
filter.valueFlow.map { it.unAccent() },
|
||||
selectedTagId,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue