From c94c820efbed30da4eda7eb02d6121bd0eb9eb67 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Thu, 17 Apr 2025 22:37:44 +0200 Subject: [PATCH] Add inventory item detail detail (basic) --- .../desktop/lwa/DataSyncViewModel.kt | 1 - .../com/pixelized/desktop/lwa/Module.kt | 8 ++ .../lwa/ui/composable/blur/BlurContent.kt | 12 +- .../character/inventory/InventoryDialog.kt | 8 ++ .../inventory/InventoryDialogFactory.kt | 5 + .../inventory/InventoryDialogViewModel.kt | 7 + .../character/item/ItemDetailDialog.kt | 128 ++++++++++++++++++ .../character/item/ItemDetailDialogFactory.kt | 24 ++++ .../item/ItemDetailDialogViewModel.kt | 34 +++++ .../composable/character/purse/PurseDialog.kt | 5 +- .../composable/image/DesaturatedAsyncImage.kt | 57 ++++++++ .../inventory/CharacterDetailInventory.kt | 19 ++- .../CharacterDetailInventoryFactory.kt | 8 +- .../detail/inventory/item/InventoryItem.kt | 50 ++++++- .../ui/screen/gamemaster/common/tag/GMTag.kt | 58 ++++++-- .../gamemaster/common/tag/GMTagFactory.kt | 3 + .../gamemaster/item/edit/GMItemEditFactory.kt | 73 +++++----- .../gamemaster/item/edit/GMItemEditPage.kt | 4 +- .../gamemaster/item/list/GMItemFactory.kt | 4 +- .../desktop/lwa/ui/theme/size/LwaSize.kt | 2 +- .../lwa/utils/rememberBackgroundGradient.kt | 22 +++ .../lwa/utils/rememberSaturationFilter.kt | 17 +++ .../server/lwa/model/item/ItemStore.kt | 8 +- .../pixelized/shared/lwa/model/item/Item.kt | 2 +- .../model/item/factory/ItemJsonFactoryV1.kt | 4 +- .../com/pixelized/shared/lwa/model/tag/Tag.kt | 1 + .../shared/lwa/model/tag/TagJsonFactory.kt | 2 + .../shared/lwa/model/tag/TagJsonV1.kt | 1 + 28 files changed, 490 insertions(+), 77 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/DesaturatedAsyncImage.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberBackgroundGradient.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberSaturationFilter.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index 2473584..4faeedd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -61,7 +61,6 @@ class DataSyncViewModel( networkRepository.status .filter { status -> status == NetworkRepository.Status.CONNECTED } .flatMapLatest { campaignRepository.campaignFlow().map { it.instances } } - .distinctUntilChanged() .onEach { instances -> instances.forEach { characterSheetId -> characterRepository.updateCharacterSheet( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 520c14d..db2533a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -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.diminished.CharacterSheetDiminishedDialogFactory 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.PurseDialogViewModel import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel @@ -149,6 +153,8 @@ val factoryDependencies factoryOf(::CharacterSheetCharacteristicDialogFactory) factoryOf(::CharacterSheetDiminishedDialogFactory) factoryOf(::CharacterSheetAlterationDialogFactory) + factoryOf(::InventoryDialogFactory) + factoryOf(::ItemDetailDialogFactory) factoryOf(::PurseDialogFactory) factoryOf(::TextMessageFactory) factoryOf(::LevelUpFactory) @@ -175,6 +181,8 @@ val viewModelDependencies viewModelOf(::CharacterSheetDiminishedDialogViewModel) viewModelOf(::CharacterSheetCharacteristicDialogViewModel) viewModelOf(::CharacterSheetAlterationDialogViewModel) + viewModelOf(::InventoryDialogViewModel) + viewModelOf(::ItemDetailDialogViewModel) viewModelOf(::PurseDialogViewModel) viewModelOf(::CampaignChatViewModel) viewModelOf(::SettingsViewModel) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/blur/BlurContent.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/blur/BlurContent.kt index 2c40cc2..0e7e479 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/blur/BlurContent.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/blur/BlurContent.kt @@ -8,7 +8,8 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable 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.ui.Modifier 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 com.pixelized.desktop.lwa.ui.theme.color.LwaColorPalette +import kotlin.math.max @Stable class BlurContentController( @@ -25,15 +27,15 @@ class BlurContentController( val blurredRadius: Dp = 8.dp, val scrimColor: Color = LwaColorPalette.DefaultScrimColor, ) { - private val _blurred = mutableStateOf(blurred) - val isBlurred: State get() = _blurred + private val layer = mutableIntStateOf(if (blurred) 1 else 0) + val isBlurred: State = derivedStateOf { layer.value != 0 } fun show() { - _blurred.value = true + layer.value += 1 } fun hide() { - _blurred.value = false + layer.value = max(layer.value - 1, 0) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialog.kt new file mode 100644 index 0000000..d2dfc6a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialog.kt @@ -0,0 +1,8 @@ +package com.pixelized.desktop.lwa.ui.composable.character.inventory + +import androidx.compose.runtime.Composable + +@Composable +fun InventoryDialog() { + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogFactory.kt new file mode 100644 index 0000000..37d375e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogFactory.kt @@ -0,0 +1,5 @@ +package com.pixelized.desktop.lwa.ui.composable.character.inventory + +class InventoryDialogFactory { + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogViewModel.kt new file mode 100644 index 0000000..e567ccd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/inventory/InventoryDialogViewModel.kt @@ -0,0 +1,7 @@ +package com.pixelized.desktop.lwa.ui.composable.character.inventory + +import androidx.lifecycle.ViewModel + +class InventoryDialogViewModel: ViewModel() { + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialog.kt new file mode 100644 index 0000000..5782e5a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialog.kt @@ -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, + 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, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogFactory.kt new file mode 100644 index 0000000..f92dd65 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogFactory.kt @@ -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, + 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, + ), + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogViewModel.kt new file mode 100644 index 0000000..543d282 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/item/ItemDetailDialogViewModel.kt @@ -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(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 } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt index c38c47c..a897da3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/purse/PurseDialog.kt @@ -92,7 +92,8 @@ fun PurseDialog( } @Composable -fun PurseContent( +private fun PurseContent( + modifier: Modifier = Modifier, dialog: PurseDialogUio, onConfirm: (PurseDialogUio) -> Unit, onSwapSign: (PurseDialogUio) -> Unit, @@ -119,7 +120,7 @@ fun PurseContent( DecoratedBox { Surface { Column( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = modifier.padding(horizontal = 8.dp), verticalArrangement = Arrangement.spacedBy(space = 8.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/DesaturatedAsyncImage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/DesaturatedAsyncImage.kt new file mode 100644 index 0000000..aff7164 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/image/DesaturatedAsyncImage.kt @@ -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) + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt index cdd94f3..aeea9ad 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventory.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items 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 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.PurseDialogViewModel import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler @@ -80,6 +81,7 @@ fun CharacterDetailInventory( paddings: PaddingValues = CharacterDetailInventoryDefault.padding, spacing: Dp = CharacterDetailInventoryDefault.spacing, purseViewModel: PurseDialogViewModel = koinViewModel(), + itemDetailDialogViewModel: ItemDetailDialogViewModel = koinViewModel(), inventory: State, ) { val blur = LocalBlurController.current @@ -96,6 +98,10 @@ fun CharacterDetailInventory( onPurse = { blur.show() 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( error = purseViewModel.error, ) @@ -131,6 +145,7 @@ private fun CharacterDetailInventoryContent( spacing: Dp, inventory: CharacterDetailInventoryUio, onPurse: (String) -> Unit, + onItem: (InventoryItemUio) -> Unit, ) { Box( modifier = modifier, @@ -191,7 +206,7 @@ private fun CharacterDetailInventoryContent( .animateItem() .fillMaxWidth(), item = it, - onClick = { }, + onClick = { onItem(it) }, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt index ab830e2..b7940c6 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/CharacterDetailInventoryFactory.kt @@ -50,7 +50,7 @@ class CharacterDetailInventoryFactory( filter = filterField, purse = inventory?.purse, inventory = inventory?.items, - items = items.filterValues { it.metadata.name.unAccent().contains(filter, true) }, + items = items.filterValues { it.metadata.label.unAccent().contains(filter, true) }, ) }.stateIn( scope = scope, @@ -78,10 +78,12 @@ class CharacterDetailInventoryFactory( filter = filter, items = inventory ?.mapNotNull { - val label = items[it.itemId]?.metadata?.name ?: return@mapNotNull null + val item = items[it.itemId] ?: return@mapNotNull null InventoryItemUio( inventoryId = it.inventoryId, - label = label, + itemId = it.itemId, + label = item.metadata.label, + count = it.count, equipped = it.equipped, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt index 6fb5656..7b5709b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryItem.kt @@ -1,7 +1,16 @@ 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.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -10,9 +19,13 @@ import androidx.compose.material.Text import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.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 com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.utils.extention.ribbon @@ -20,20 +33,26 @@ import com.pixelized.desktop.lwa.utils.extention.ribbon @Stable data class InventoryItemUio( val inventoryId: String, + val itemId: String, val label: String, + val count: Int, val equipped: Boolean, ) @Stable object GMCharacterPreviewDefault { @Stable - val paddings = PaddingValues(horizontal = 16.dp) + val paddings = PaddingValues(horizontal = 16.dp, vertical = 4.dp) + + @Stable + val spacing: Dp = 4.dp } @Composable fun InventoryItem( modifier: Modifier = Modifier, padding: PaddingValues = GMCharacterPreviewDefault.paddings, + spacing: Dp = GMCharacterPreviewDefault.spacing, item: InventoryItemUio, onClick: () -> Unit, ) { @@ -51,10 +70,39 @@ fun InventoryItem( .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) + } + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.lwa.typography.base.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = "x${it}", + ) + } + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt index 8fe5de8..ac47243 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt @@ -2,7 +2,9 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Button @@ -25,6 +27,7 @@ data class GMTagUio( val id: String, val label: String, val highlight: Boolean, + val meta: Boolean, ) @Stable @@ -52,16 +55,31 @@ fun GMTag( shape = shape, elevation = elevation, ) { - Text( + Row( modifier = Modifier .clickable(enabled = onTag != null) { onTag?.invoke() } .padding(paddingValues = padding), - style = MaterialTheme.lwa.typography.base.caption, - color = animatedColor.value, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - text = tag.label, - ) + horizontalArrangement = Arrangement.spacedBy(space = 1.dp), + ) { + if (tag.meta) { + Text( + 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, onClick = { onTag?.invoke() }, ) { - Text( + Row( modifier = Modifier.padding(paddingValues = padding), - color = animatedColor.value, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - text = tag.label, - ) + horizontalArrangement = Arrangement.spacedBy(space = 1.dp), + ) { + if (tag.meta) { + 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, + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt index 238b93d..a585516 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt @@ -15,6 +15,7 @@ class GMTagFactory { id = tag.id, label = tag.label, highlight = selectedTagIds.contains(tag.id), + meta = tag.meta, ) } .sortedWith( @@ -32,6 +33,7 @@ class GMTagFactory { id = tag.id, label = tag.label, highlight = tag.id == selectedTagId, + meta = tag.meta, ) } .sortedWith( @@ -47,6 +49,7 @@ class GMTagFactory { id = tag.id, label = tag.label, highlight = tag.id == selectedTagId, + meta = tag.meta, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt index 61ab2a3..9c8d117 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt @@ -23,14 +23,14 @@ class GMItemEditFactory( item: Item?, tags: Collection, ): GMItemEditPageUio { - val idFlow = MutableStateFlow(item?.id ?: "") - val labelFlow = MutableStateFlow(item?.metadata?.name ?: "") - val descriptionFlow = MutableStateFlow(item?.metadata?.description ?: "") - val imageFlow = MutableStateFlow(item?.metadata?.image ?: "") - val thumbnailFlow = MutableStateFlow(item?.metadata?.thumbnail ?: "") - val stackableFlow = MutableStateFlow(item?.options?.stackable ?: false) - val equipableFlow = MutableStateFlow(item?.options?.equipable ?: false) - val consumableFlow = MutableStateFlow(item?.options?.consumable ?: false) + val idFlow = createFlows(initialValue = item?.id ?: "") + val labelFlow = createFlows(initialValue = item?.metadata?.label ?: "") + val descriptionFlow = createFlows(initialValue = item?.metadata?.description ?: "") + val imageFlow = createFlows(initialValue = item?.metadata?.image ?: "") + val thumbnailFlow = createFlows(initialValue = item?.metadata?.thumbnail ?: "") + val stackableFlow = MutableStateFlow(value = item?.options?.stackable ?: false) + val equipableFlow = MutableStateFlow(value = item?.options?.equipable ?: false) + val consumableFlow = MutableStateFlow(value = item?.options?.consumable ?: false) val tagFlow = MutableStateFlow( tagFactory.convertToGMTagItemUio( @@ -40,45 +40,21 @@ class GMItemEditFactory( ) return GMItemEditPageUio( - id = LwaTextFieldUio( + id = idFlow.createLwaTextField( enable = originId == null, - isError = MutableStateFlow(false), label = getString(Res.string.game_master__item__edit_id), - valueFlow = idFlow, - placeHolder = null, - onValueChange = { idFlow.value = it }, ), - label = LwaTextFieldUio( - enable = true, - isError = MutableStateFlow(false), + label = labelFlow.createLwaTextField( label = getString(Res.string.game_master__item__edit_label), - valueFlow = labelFlow, - placeHolder = null, - onValueChange = { labelFlow.value = it }, ), - description = LwaTextFieldUio( - enable = true, - isError = MutableStateFlow(false), + description = descriptionFlow.createLwaTextField( label = getString(Res.string.game_master__item__edit_description), - valueFlow = descriptionFlow, - placeHolder = null, - onValueChange = { descriptionFlow.value = it }, ), - image = LwaTextFieldUio( - enable = true, - isError = MutableStateFlow(false), + image = imageFlow.createLwaTextField( label = getString(Res.string.game_master__item__edit_image), - valueFlow = imageFlow, - placeHolder = null, - onValueChange = { descriptionFlow.value = it }, ), - thumbnail = LwaTextFieldUio( - enable = true, - isError = MutableStateFlow(false), + thumbnail = thumbnailFlow.createLwaTextField( label = getString(Res.string.game_master__item__edit_thumbnail), - valueFlow = thumbnailFlow, - placeHolder = null, - onValueChange = { descriptionFlow.value = it }, ), equipable = LwaCheckBoxUio( checked = equipableFlow, @@ -104,7 +80,7 @@ class GMItemEditFactory( return Item( id = form.id.valueFlow.value, metadata = Item.MetaData( - name = form.label.valueFlow.value, + label = form.label.valueFlow.value, description = form.description.valueFlow.value, image = form.image.valueFlow.value, thumbnail = form.thumbnail.valueFlow.value, @@ -120,4 +96,25 @@ class GMItemEditFactory( alterations = emptyList(), // TODO, ) } + + private fun createFlows( + initialValue: String = "", + initialError: Boolean = false, + ): Pair, MutableStateFlow> { + return MutableStateFlow(value = initialValue) to MutableStateFlow(value = initialError) + } + + private fun Pair, MutableStateFlow>.createLwaTextField( + enable: Boolean = true, + label: String, + ): LwaTextFieldUio { + return LwaTextFieldUio( + enable = enable, + isError = second, + label = label, + valueFlow = first, + placeHolder = null, + onValueChange = { first.value = it }, + ) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt index c3055a6..96fbd80 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt @@ -245,7 +245,7 @@ private fun GMItemEditContent( .animateItem() .fillMaxWidth(), field = it.image, - singleLine = false, + singleLine = true, ) } item( @@ -256,7 +256,7 @@ private fun GMItemEditContent( .animateItem() .fillMaxWidth(), field = it.thumbnail, - singleLine = false, + singleLine = true, ) } item( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt index b48d99c..6a40880 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt @@ -15,7 +15,7 @@ class GMItemFactory( selectedTagId: String?, ): List { return items.filter { - val matchName = it.metadata.name.unAccent().contains( + val matchName = it.metadata.label.unAccent().contains( other = unAccentFilter, ignoreCase = true ) @@ -35,7 +35,7 @@ class GMItemFactory( .map { item -> GMItemUio( itemId = item.id, - label = item.metadata.name, + label = item.metadata.label, tags = item.tags.mapNotNull { tags[it]?.let { tag -> tagFactory.convertToGMTagItemUio( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt index cec5868..31aecb0 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp - +@Stable data class LwaSize( val portrait: Portrait, val sheet: Sheet, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberBackgroundGradient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberBackgroundGradient.kt new file mode 100644 index 0000000..a33468e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberBackgroundGradient.kt @@ -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), + ) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberSaturationFilter.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberSaturationFilter.kt new file mode 100644 index 0000000..f64b4be --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberSaturationFilter.kt @@ -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) } + ) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt index d0eaa48..af04b54 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt @@ -80,7 +80,7 @@ class ItemStore( throw JsonConversionException(root = exception) } } - ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) + ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.label }) ?: emptyList() } @@ -113,7 +113,7 @@ class ItemStore( code = APIResponse.ErrorCode.ItemId, ) } - if (item.metadata.name.isEmpty()) { + if (item.metadata.label.isEmpty()) { throw BusinessException( message = "Item 'name' is a mandatory field.", code = APIResponse.ErrorCode.ItemName, @@ -146,7 +146,7 @@ class ItemStore( } } .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.name + it.metadata.label }) } } @@ -173,7 +173,7 @@ class ItemStore( item.removeIf { it.id == id } } .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.name + it.metadata.label }) } } diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt index e2ab8d9..aa317dd 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt @@ -8,7 +8,7 @@ data class Item( val alterations: List, ) { data class MetaData( - val name: String, + val label: String, val description: String, val thumbnail: String?, val image: String?, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt index b99ed1f..0577ace 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt @@ -9,7 +9,7 @@ class ItemJsonFactoryV1 { return Item( id = json.id, metadata = Item.MetaData( - name = json.metadata.name, + label = json.metadata.name, description = json.metadata.description, image = json.metadata.image, thumbnail = json.metadata.thumbnail, @@ -28,7 +28,7 @@ class ItemJsonFactoryV1 { return ItemJsonV1( id = item.id, metadata = ItemJsonV1.ItemMetadataJsonV1( - name = item.metadata.name, + name = item.metadata.label, description = item.metadata.description, image = item.metadata.image, thumbnail = item.metadata.thumbnail, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt index 545ad9f..72b8633 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt @@ -3,4 +3,5 @@ package com.pixelized.shared.lwa.model.tag data class Tag( val id: String, val label: String, + val meta: Boolean, ) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt index 874d30c..747e690 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt @@ -9,6 +9,7 @@ class TagJsonFactory { is TagJsonV1 -> Tag( id = json.id, label = json.label, + meta = json.meta ?: false, ) } } @@ -19,6 +20,7 @@ class TagJsonFactory { return TagJsonV1( id = tag.id, label = tag.label, + meta = tag.meta.takeIf { it }, ) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt index 500456a..7700081 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable data class TagJsonV1( override val id: String, val label: String, + val meta: Boolean?, ) : TagJson \ No newline at end of file