Add inventory item detail detail (basic)

This commit is contained in:
Thomas Andres Gomez 2025-04-17 22:37:44 +02:00
parent 05a376aea8
commit c94c820efb
28 changed files with 490 additions and 77 deletions

View file

@ -61,7 +61,6 @@ class DataSyncViewModel(
networkRepository.status networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED } .filter { status -> status == NetworkRepository.Status.CONNECTED }
.flatMapLatest { campaignRepository.campaignFlow().map { it.instances } } .flatMapLatest { campaignRepository.campaignFlow().map { it.instances } }
.distinctUntilChanged()
.onEach { instances -> .onEach { instances ->
instances.forEach { characterSheetId -> instances.forEach { characterSheetId ->
characterRepository.updateCharacterSheet( characterRepository.updateCharacterSheet(

View file

@ -25,6 +25,10 @@ import com.pixelized.desktop.lwa.ui.composable.character.characteristic.Characte
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogFactory import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.item.ItemDetailDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogFactory import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogFactory
import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogViewModel
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel
@ -149,6 +153,8 @@ val factoryDependencies
factoryOf(::CharacterSheetCharacteristicDialogFactory) factoryOf(::CharacterSheetCharacteristicDialogFactory)
factoryOf(::CharacterSheetDiminishedDialogFactory) factoryOf(::CharacterSheetDiminishedDialogFactory)
factoryOf(::CharacterSheetAlterationDialogFactory) factoryOf(::CharacterSheetAlterationDialogFactory)
factoryOf(::InventoryDialogFactory)
factoryOf(::ItemDetailDialogFactory)
factoryOf(::PurseDialogFactory) factoryOf(::PurseDialogFactory)
factoryOf(::TextMessageFactory) factoryOf(::TextMessageFactory)
factoryOf(::LevelUpFactory) factoryOf(::LevelUpFactory)
@ -175,6 +181,8 @@ val viewModelDependencies
viewModelOf(::CharacterSheetDiminishedDialogViewModel) viewModelOf(::CharacterSheetDiminishedDialogViewModel)
viewModelOf(::CharacterSheetCharacteristicDialogViewModel) viewModelOf(::CharacterSheetCharacteristicDialogViewModel)
viewModelOf(::CharacterSheetAlterationDialogViewModel) viewModelOf(::CharacterSheetAlterationDialogViewModel)
viewModelOf(::InventoryDialogViewModel)
viewModelOf(::ItemDetailDialogViewModel)
viewModelOf(::PurseDialogViewModel) viewModelOf(::PurseDialogViewModel)
viewModelOf(::CampaignChatViewModel) viewModelOf(::CampaignChatViewModel)
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)

View file

@ -8,7 +8,8 @@ import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment import androidx.compose.ui.draw.BlurredEdgeTreatment
@ -18,6 +19,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.color.LwaColorPalette import com.pixelized.desktop.lwa.ui.theme.color.LwaColorPalette
import kotlin.math.max
@Stable @Stable
class BlurContentController( class BlurContentController(
@ -25,15 +27,15 @@ class BlurContentController(
val blurredRadius: Dp = 8.dp, val blurredRadius: Dp = 8.dp,
val scrimColor: Color = LwaColorPalette.DefaultScrimColor, val scrimColor: Color = LwaColorPalette.DefaultScrimColor,
) { ) {
private val _blurred = mutableStateOf(blurred) private val layer = mutableIntStateOf(if (blurred) 1 else 0)
val isBlurred: State<Boolean> get() = _blurred val isBlurred: State<Boolean> = derivedStateOf { layer.value != 0 }
fun show() { fun show() {
_blurred.value = true layer.value += 1
} }
fun hide() { fun hide() {
_blurred.value = false layer.value = max(layer.value - 1, 0)
} }
} }

View file

@ -0,0 +1,8 @@
package com.pixelized.desktop.lwa.ui.composable.character.inventory
import androidx.compose.runtime.Composable
@Composable
fun InventoryDialog() {
}

View file

@ -0,0 +1,5 @@
package com.pixelized.desktop.lwa.ui.composable.character.inventory
class InventoryDialogFactory {
}

View file

@ -0,0 +1,7 @@
package com.pixelized.desktop.lwa.ui.composable.character.inventory
import androidx.lifecycle.ViewModel
class InventoryDialogViewModel: ViewModel() {
}

View file

@ -0,0 +1,128 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
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.fillMaxSize
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.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
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.utils.extention.onPreviewEscape
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
@Stable
data class ItemDetailDialogUio(
val itemId: String,
val label: String,
val description: String,
val image: String?,
val option: OptionUio,
) {
@Stable
data class OptionUio(
val equipable: Boolean,
val consumable: Boolean,
)
}
@Stable
object ItemDetailDialogDefault {
@Stable
val paddings = PaddingValues(all = 16.dp)
}
@Composable
fun ItemDetailDialog(
dialog: State<ItemDetailDialogUio?>,
paddings: PaddingValues = ItemDetailDialogDefault.paddings,
onDismissRequest: () -> Unit,
) {
dialog.value?.let {
Dialog(
onDismissRequest = onDismissRequest,
content = {
ItemDetailDialogContent(
dialog = it,
paddings = paddings,
onDismissRequest = onDismissRequest,
)
}
)
}
}
@Composable
private fun ItemDetailDialogContent(
modifier: Modifier = Modifier,
paddings: PaddingValues,
dialog: ItemDetailDialogUio,
onDismissRequest: () -> Unit,
) {
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.onPreviewEscape(
escape = onDismissRequest,
enter = onDismissRequest,
)
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,
) {
DecoratedBox {
Surface {
Box(
modifier = Modifier.padding(paddingValues = paddings)
) {
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,
)
Text(
style = MaterialTheme.typography.body1,
text = dialog.description,
)
}
}
}
}
}
}

View file

@ -0,0 +1,24 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
import com.pixelized.shared.lwa.model.item.Item
class ItemDetailDialogFactory {
fun convertToDialogUio(
items: Map<String, Item>,
itemId: String?,
): ItemDetailDialogUio? {
val item = itemId.let(items::get) ?: return null
return ItemDetailDialogUio(
itemId = item.id,
label = item.metadata.label,
description = item.metadata.description,
image = item.metadata.image,
option = ItemDetailDialogUio.OptionUio(
equipable = item.options.equipable,
consumable = item.options.consumable,
),
)
}
}

View file

@ -0,0 +1,34 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.item.ItemRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class ItemDetailDialogViewModel(
itemRepository: ItemRepository,
factory: ItemDetailDialogFactory,
) : ViewModel() {
private val selectedItemId = MutableStateFlow<String?>(null)
val itemDialog = combine(
itemRepository.itemFlow, selectedItemId,
transform = { items, itemId -> factory.convertToDialogUio(items, itemId) }
).stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null
)
fun showItemDialog(itemId: String?) {
selectedItemId.update { itemId }
}
fun hideItemDialog() {
selectedItemId.update { null }
}
}

View file

@ -92,7 +92,8 @@ fun PurseDialog(
} }
@Composable @Composable
fun PurseContent( private fun PurseContent(
modifier: Modifier = Modifier,
dialog: PurseDialogUio, dialog: PurseDialogUio,
onConfirm: (PurseDialogUio) -> Unit, onConfirm: (PurseDialogUio) -> Unit,
onSwapSign: (PurseDialogUio) -> Unit, onSwapSign: (PurseDialogUio) -> Unit,
@ -119,7 +120,7 @@ fun PurseContent(
DecoratedBox { DecoratedBox {
Surface { Surface {
Column( Column(
modifier = Modifier.padding(horizontal = 8.dp), modifier = modifier.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 8.dp), verticalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {

View file

@ -0,0 +1,57 @@
package com.pixelized.desktop.lwa.ui.composable.image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter.Companion.DefaultTransform
import coil3.compose.AsyncImagePainter.State
import com.pixelized.desktop.lwa.utils.rememberBackgroundGradient
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
@Composable
fun DesaturatedAsyncImage(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
transform: (State) -> State = DefaultTransform,
onState: ((State) -> Unit)? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
overlay: Brush? = rememberBackgroundGradient(),
colorFilter: ColorFilter? = rememberSaturationFilter(saturation = 0f),
filterQuality: FilterQuality = FilterQuality.Low,
clipToBounds: Boolean = true,
) {
Box(modifier = modifier) {
AsyncImage(
model = model,
contentDescription = contentDescription,
modifier = Modifier.matchParentSize(),
transform = transform,
onState = onState,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality,
clipToBounds = clipToBounds,
)
if (overlay != null) {
Box(
modifier = Modifier
.matchParentSize()
.background(brush = overlay)
)
}
}
}

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -31,6 +30,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalBlurController import com.pixelized.desktop.lwa.LocalBlurController
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 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.character.purse.PurseDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
@ -80,6 +81,7 @@ fun CharacterDetailInventory(
paddings: PaddingValues = CharacterDetailInventoryDefault.padding, paddings: PaddingValues = CharacterDetailInventoryDefault.padding,
spacing: Dp = CharacterDetailInventoryDefault.spacing, spacing: Dp = CharacterDetailInventoryDefault.spacing,
purseViewModel: PurseDialogViewModel = koinViewModel(), purseViewModel: PurseDialogViewModel = koinViewModel(),
itemDetailDialogViewModel: ItemDetailDialogViewModel = koinViewModel(),
inventory: State<CharacterDetailInventoryUio?>, inventory: State<CharacterDetailInventoryUio?>,
) { ) {
val blur = LocalBlurController.current val blur = LocalBlurController.current
@ -96,6 +98,10 @@ fun CharacterDetailInventory(
onPurse = { onPurse = {
blur.show() blur.show()
purseViewModel.showPurseDialog(characterSheetId = it) purseViewModel.showPurseDialog(characterSheetId = it)
},
onItem = {
blur.show()
itemDetailDialogViewModel.showItemDialog(itemId = it.itemId)
} }
) )
} }
@ -119,6 +125,14 @@ fun CharacterDetailInventory(
} }
) )
ItemDetailDialog(
dialog = itemDetailDialogViewModel.itemDialog.collectAsState(),
onDismissRequest = {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
}
)
ErrorSnackHandler( ErrorSnackHandler(
error = purseViewModel.error, error = purseViewModel.error,
) )
@ -131,6 +145,7 @@ private fun CharacterDetailInventoryContent(
spacing: Dp, spacing: Dp,
inventory: CharacterDetailInventoryUio, inventory: CharacterDetailInventoryUio,
onPurse: (String) -> Unit, onPurse: (String) -> Unit,
onItem: (InventoryItemUio) -> Unit,
) { ) {
Box( Box(
modifier = modifier, modifier = modifier,
@ -191,7 +206,7 @@ private fun CharacterDetailInventoryContent(
.animateItem() .animateItem()
.fillMaxWidth(), .fillMaxWidth(),
item = it, item = it,
onClick = { }, onClick = { onItem(it) },
) )
} }
} }

View file

@ -50,7 +50,7 @@ class CharacterDetailInventoryFactory(
filter = filterField, filter = filterField,
purse = inventory?.purse, purse = inventory?.purse,
inventory = inventory?.items, inventory = inventory?.items,
items = items.filterValues { it.metadata.name.unAccent().contains(filter, true) }, items = items.filterValues { it.metadata.label.unAccent().contains(filter, true) },
) )
}.stateIn( }.stateIn(
scope = scope, scope = scope,
@ -78,10 +78,12 @@ class CharacterDetailInventoryFactory(
filter = filter, filter = filter,
items = inventory items = inventory
?.mapNotNull { ?.mapNotNull {
val label = items[it.itemId]?.metadata?.name ?: return@mapNotNull null val item = items[it.itemId] ?: return@mapNotNull null
InventoryItemUio( InventoryItemUio(
inventoryId = it.inventoryId, inventoryId = it.inventoryId,
label = label, itemId = it.itemId,
label = item.metadata.label,
count = it.count,
equipped = it.equipped, equipped = it.equipped,
) )
} }

View file

@ -1,7 +1,16 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item 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.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -10,9 +19,13 @@ import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.ribbon import com.pixelized.desktop.lwa.utils.extention.ribbon
@ -20,20 +33,26 @@ import com.pixelized.desktop.lwa.utils.extention.ribbon
@Stable @Stable
data class InventoryItemUio( data class InventoryItemUio(
val inventoryId: String, val inventoryId: String,
val itemId: String,
val label: String, val label: String,
val count: Int,
val equipped: Boolean, val equipped: Boolean,
) )
@Stable @Stable
object GMCharacterPreviewDefault { object GMCharacterPreviewDefault {
@Stable @Stable
val paddings = PaddingValues(horizontal = 16.dp) val paddings = PaddingValues(horizontal = 16.dp, vertical = 4.dp)
@Stable
val spacing: Dp = 4.dp
} }
@Composable @Composable
fun InventoryItem( fun InventoryItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
padding: PaddingValues = GMCharacterPreviewDefault.paddings, padding: PaddingValues = GMCharacterPreviewDefault.paddings,
spacing: Dp = GMCharacterPreviewDefault.spacing,
item: InventoryItemUio, item: InventoryItemUio,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -51,10 +70,39 @@ fun InventoryItem(
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.padding(paddingValues = padding) .padding(paddingValues = padding)
.then(other = modifier), .then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = spacing),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1, style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = item.label, 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)
}
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "x${it}",
)
}
}
} }
} }

View file

@ -2,7 +2,9 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button import androidx.compose.material.Button
@ -25,6 +27,7 @@ data class GMTagUio(
val id: String, val id: String,
val label: String, val label: String,
val highlight: Boolean, val highlight: Boolean,
val meta: Boolean,
) )
@Stable @Stable
@ -52,16 +55,31 @@ fun GMTag(
shape = shape, shape = shape,
elevation = elevation, elevation = elevation,
) { ) {
Text( Row(
modifier = Modifier modifier = Modifier
.clickable(enabled = onTag != null) { onTag?.invoke() } .clickable(enabled = onTag != null) { onTag?.invoke() }
.padding(paddingValues = padding), .padding(paddingValues = padding),
style = MaterialTheme.lwa.typography.base.caption, horizontalArrangement = Arrangement.spacedBy(space = 1.dp),
color = animatedColor.value, ) {
overflow = TextOverflow.Ellipsis, if (tag.meta) {
maxLines = 1, Text(
text = tag.label, modifier = Modifier.alignByBaseline(),
) style = MaterialTheme.lwa.typography.base.caption,
color = animatedColor.value,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "",
)
}
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.caption,
color = animatedColor.value,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = tag.label,
)
}
} }
} }
@ -87,12 +105,26 @@ fun GMTagButton(
enabled = onTag != null, enabled = onTag != null,
onClick = { onTag?.invoke() }, onClick = { onTag?.invoke() },
) { ) {
Text( Row(
modifier = Modifier.padding(paddingValues = padding), modifier = Modifier.padding(paddingValues = padding),
color = animatedColor.value, horizontalArrangement = Arrangement.spacedBy(space = 1.dp),
overflow = TextOverflow.Ellipsis, ) {
maxLines = 1, if (tag.meta) {
text = tag.label, Text(
) modifier = Modifier.alignByBaseline(),
color = animatedColor.value,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "",
)
}
Text(
modifier = Modifier.alignByBaseline(),
color = animatedColor.value,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = tag.label,
)
}
} }
} }

View file

@ -15,6 +15,7 @@ class GMTagFactory {
id = tag.id, id = tag.id,
label = tag.label, label = tag.label,
highlight = selectedTagIds.contains(tag.id), highlight = selectedTagIds.contains(tag.id),
meta = tag.meta,
) )
} }
.sortedWith( .sortedWith(
@ -32,6 +33,7 @@ class GMTagFactory {
id = tag.id, id = tag.id,
label = tag.label, label = tag.label,
highlight = tag.id == selectedTagId, highlight = tag.id == selectedTagId,
meta = tag.meta,
) )
} }
.sortedWith( .sortedWith(
@ -47,6 +49,7 @@ class GMTagFactory {
id = tag.id, id = tag.id,
label = tag.label, label = tag.label,
highlight = tag.id == selectedTagId, highlight = tag.id == selectedTagId,
meta = tag.meta,
) )
} }
} }

View file

@ -23,14 +23,14 @@ class GMItemEditFactory(
item: Item?, item: Item?,
tags: Collection<Tag>, tags: Collection<Tag>,
): GMItemEditPageUio { ): GMItemEditPageUio {
val idFlow = MutableStateFlow(item?.id ?: "") val idFlow = createFlows(initialValue = item?.id ?: "")
val labelFlow = MutableStateFlow(item?.metadata?.name ?: "") val labelFlow = createFlows(initialValue = item?.metadata?.label ?: "")
val descriptionFlow = MutableStateFlow(item?.metadata?.description ?: "") val descriptionFlow = createFlows(initialValue = item?.metadata?.description ?: "")
val imageFlow = MutableStateFlow(item?.metadata?.image ?: "") val imageFlow = createFlows(initialValue = item?.metadata?.image ?: "")
val thumbnailFlow = MutableStateFlow(item?.metadata?.thumbnail ?: "") val thumbnailFlow = createFlows(initialValue = item?.metadata?.thumbnail ?: "")
val stackableFlow = MutableStateFlow(item?.options?.stackable ?: false) val stackableFlow = MutableStateFlow(value = item?.options?.stackable ?: false)
val equipableFlow = MutableStateFlow(item?.options?.equipable ?: false) val equipableFlow = MutableStateFlow(value = item?.options?.equipable ?: false)
val consumableFlow = MutableStateFlow(item?.options?.consumable ?: false) val consumableFlow = MutableStateFlow(value = item?.options?.consumable ?: false)
val tagFlow = MutableStateFlow( val tagFlow = MutableStateFlow(
tagFactory.convertToGMTagItemUio( tagFactory.convertToGMTagItemUio(
@ -40,45 +40,21 @@ class GMItemEditFactory(
) )
return GMItemEditPageUio( return GMItemEditPageUio(
id = LwaTextFieldUio( id = idFlow.createLwaTextField(
enable = originId == null, enable = originId == null,
isError = MutableStateFlow(false),
label = getString(Res.string.game_master__item__edit_id), label = getString(Res.string.game_master__item__edit_id),
valueFlow = idFlow,
placeHolder = null,
onValueChange = { idFlow.value = it },
), ),
label = LwaTextFieldUio( label = labelFlow.createLwaTextField(
enable = true,
isError = MutableStateFlow(false),
label = getString(Res.string.game_master__item__edit_label), label = getString(Res.string.game_master__item__edit_label),
valueFlow = labelFlow,
placeHolder = null,
onValueChange = { labelFlow.value = it },
), ),
description = LwaTextFieldUio( description = descriptionFlow.createLwaTextField(
enable = true,
isError = MutableStateFlow(false),
label = getString(Res.string.game_master__item__edit_description), label = getString(Res.string.game_master__item__edit_description),
valueFlow = descriptionFlow,
placeHolder = null,
onValueChange = { descriptionFlow.value = it },
), ),
image = LwaTextFieldUio( image = imageFlow.createLwaTextField(
enable = true,
isError = MutableStateFlow(false),
label = getString(Res.string.game_master__item__edit_image), label = getString(Res.string.game_master__item__edit_image),
valueFlow = imageFlow,
placeHolder = null,
onValueChange = { descriptionFlow.value = it },
), ),
thumbnail = LwaTextFieldUio( thumbnail = thumbnailFlow.createLwaTextField(
enable = true,
isError = MutableStateFlow(false),
label = getString(Res.string.game_master__item__edit_thumbnail), label = getString(Res.string.game_master__item__edit_thumbnail),
valueFlow = thumbnailFlow,
placeHolder = null,
onValueChange = { descriptionFlow.value = it },
), ),
equipable = LwaCheckBoxUio( equipable = LwaCheckBoxUio(
checked = equipableFlow, checked = equipableFlow,
@ -104,7 +80,7 @@ class GMItemEditFactory(
return Item( return Item(
id = form.id.valueFlow.value, id = form.id.valueFlow.value,
metadata = Item.MetaData( metadata = Item.MetaData(
name = form.label.valueFlow.value, label = form.label.valueFlow.value,
description = form.description.valueFlow.value, description = form.description.valueFlow.value,
image = form.image.valueFlow.value, image = form.image.valueFlow.value,
thumbnail = form.thumbnail.valueFlow.value, thumbnail = form.thumbnail.valueFlow.value,
@ -120,4 +96,25 @@ class GMItemEditFactory(
alterations = emptyList(), // TODO, alterations = emptyList(), // TODO,
) )
} }
private fun createFlows(
initialValue: String = "",
initialError: Boolean = false,
): Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>> {
return MutableStateFlow(value = initialValue) to MutableStateFlow(value = initialError)
}
private fun Pair<MutableStateFlow<String>, MutableStateFlow<Boolean>>.createLwaTextField(
enable: Boolean = true,
label: String,
): LwaTextFieldUio {
return LwaTextFieldUio(
enable = enable,
isError = second,
label = label,
valueFlow = first,
placeHolder = null,
onValueChange = { first.value = it },
)
}
} }

View file

@ -245,7 +245,7 @@ private fun GMItemEditContent(
.animateItem() .animateItem()
.fillMaxWidth(), .fillMaxWidth(),
field = it.image, field = it.image,
singleLine = false, singleLine = true,
) )
} }
item( item(
@ -256,7 +256,7 @@ private fun GMItemEditContent(
.animateItem() .animateItem()
.fillMaxWidth(), .fillMaxWidth(),
field = it.thumbnail, field = it.thumbnail,
singleLine = false, singleLine = true,
) )
} }
item( item(

View file

@ -15,7 +15,7 @@ class GMItemFactory(
selectedTagId: String?, selectedTagId: String?,
): List<Item> { ): List<Item> {
return items.filter { return items.filter {
val matchName = it.metadata.name.unAccent().contains( val matchName = it.metadata.label.unAccent().contains(
other = unAccentFilter, other = unAccentFilter,
ignoreCase = true ignoreCase = true
) )
@ -35,7 +35,7 @@ class GMItemFactory(
.map { item -> .map { item ->
GMItemUio( GMItemUio(
itemId = item.id, itemId = item.id,
label = item.metadata.name, label = item.metadata.label,
tags = item.tags.mapNotNull { tags = item.tags.mapNotNull {
tags[it]?.let { tag -> tags[it]?.let { tag ->
tagFactory.convertToGMTagItemUio( tagFactory.convertToGMTagItemUio(

View file

@ -7,7 +7,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Stable
data class LwaSize( data class LwaSize(
val portrait: Portrait, val portrait: Portrait,
val sheet: Sheet, val sheet: Sheet,

View file

@ -0,0 +1,22 @@
package com.pixelized.desktop.lwa.utils
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Brush
@Composable
fun rememberBackgroundGradient(
from: Float = 0.5f,
to: Float = 1.0f,
): Brush {
val colorScheme = MaterialTheme.colors
return remember(colorScheme) {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = from),
colorScheme.surface.copy(alpha = to),
)
)
}
}

View file

@ -0,0 +1,17 @@
package com.pixelized.desktop.lwa.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
@Composable
fun rememberSaturationFilter(
saturation: Float = 0f,
): ColorFilter {
return remember(saturation) {
ColorFilter.colorMatrix(
ColorMatrix().also { it.setToSaturation(saturation) }
)
}
}

View file

@ -80,7 +80,7 @@ class ItemStore(
throw JsonConversionException(root = exception) throw JsonConversionException(root = exception)
} }
} }
?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.label })
?: emptyList() ?: emptyList()
} }
@ -113,7 +113,7 @@ class ItemStore(
code = APIResponse.ErrorCode.ItemId, code = APIResponse.ErrorCode.ItemId,
) )
} }
if (item.metadata.name.isEmpty()) { if (item.metadata.label.isEmpty()) {
throw BusinessException( throw BusinessException(
message = "Item 'name' is a mandatory field.", message = "Item 'name' is a mandatory field.",
code = APIResponse.ErrorCode.ItemName, code = APIResponse.ErrorCode.ItemName,
@ -146,7 +146,7 @@ class ItemStore(
} }
} }
.sortedWith(compareBy(Collator.getInstance()) { .sortedWith(compareBy(Collator.getInstance()) {
it.metadata.name it.metadata.label
}) })
} }
} }
@ -173,7 +173,7 @@ class ItemStore(
item.removeIf { it.id == id } item.removeIf { it.id == id }
} }
.sortedWith(compareBy(Collator.getInstance()) { .sortedWith(compareBy(Collator.getInstance()) {
it.metadata.name it.metadata.label
}) })
} }
} }

View file

@ -8,7 +8,7 @@ data class Item(
val alterations: List<String>, val alterations: List<String>,
) { ) {
data class MetaData( data class MetaData(
val name: String, val label: String,
val description: String, val description: String,
val thumbnail: String?, val thumbnail: String?,
val image: String?, val image: String?,

View file

@ -9,7 +9,7 @@ class ItemJsonFactoryV1 {
return Item( return Item(
id = json.id, id = json.id,
metadata = Item.MetaData( metadata = Item.MetaData(
name = json.metadata.name, label = json.metadata.name,
description = json.metadata.description, description = json.metadata.description,
image = json.metadata.image, image = json.metadata.image,
thumbnail = json.metadata.thumbnail, thumbnail = json.metadata.thumbnail,
@ -28,7 +28,7 @@ class ItemJsonFactoryV1 {
return ItemJsonV1( return ItemJsonV1(
id = item.id, id = item.id,
metadata = ItemJsonV1.ItemMetadataJsonV1( metadata = ItemJsonV1.ItemMetadataJsonV1(
name = item.metadata.name, name = item.metadata.label,
description = item.metadata.description, description = item.metadata.description,
image = item.metadata.image, image = item.metadata.image,
thumbnail = item.metadata.thumbnail, thumbnail = item.metadata.thumbnail,

View file

@ -3,4 +3,5 @@ package com.pixelized.shared.lwa.model.tag
data class Tag( data class Tag(
val id: String, val id: String,
val label: String, val label: String,
val meta: Boolean,
) )

View file

@ -9,6 +9,7 @@ class TagJsonFactory {
is TagJsonV1 -> Tag( is TagJsonV1 -> Tag(
id = json.id, id = json.id,
label = json.label, label = json.label,
meta = json.meta ?: false,
) )
} }
} }
@ -19,6 +20,7 @@ class TagJsonFactory {
return TagJsonV1( return TagJsonV1(
id = tag.id, id = tag.id,
label = tag.label, label = tag.label,
meta = tag.meta.takeIf { it },
) )
} }
} }

View file

@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
data class TagJsonV1( data class TagJsonV1(
override val id: String, override val id: String,
val label: String, val label: String,
val meta: Boolean?,
) : TagJson ) : TagJson