From 2c4519af3b98c8a66b8d06e32478145430ceb2f9 Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Mon, 12 May 2025 09:59:25 +0200 Subject: [PATCH 01/10] Fix a animation on the RibbonRoll when navigateBack --- .../ribbon/common/CharacterRibbonRoll.kt | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt index 0721040..20af6b8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonRoll.kt @@ -23,14 +23,12 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.SaverScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -39,6 +37,7 @@ import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import org.jetbrains.compose.resources.painterResource +import androidx.compose.runtime.saveable.Saver as ComposeSaver @Stable data class CharacterRibbonRollUio( @@ -64,6 +63,24 @@ class CharacterRibbonRollAnimation( animatedRotation = Animatable(rotation), animatedScale = Animatable(scale), ) + + object Saver : ComposeSaver> { + override fun restore(value: List): CharacterRibbonRollAnimation { + return CharacterRibbonRollAnimation( + animatedAlpha = Animatable(value[0]), + animatedRotation = Animatable(value[1]), + animatedScale = Animatable(value[2]), + ) + } + + override fun SaverScope.save(value: CharacterRibbonRollAnimation): List { + return listOf( + value.animatedAlpha.value, + value.animatedRotation.value, + value.animatedScale.value, + ) + } + } } @Composable @@ -85,10 +102,12 @@ fun CharacterRibbonRoll( } ) { val animation = diceIconAnimation( - key = it?.rollId ?: Unit, + key = it?.rollId, rollDelay = it?.hideDelay ?: 1000, ) - val color = animateColorAsState(targetValue = it?.tint ?: Color.Transparent) + val color = animateColorAsState( + targetValue = it?.tint ?: Color.Transparent, + ) Box( modifier = Modifier.graphicsLayer { @@ -124,10 +143,13 @@ fun CharacterRibbonRoll( @Composable private fun diceIconAnimation( - key: Any = Unit, + key: String?, rollDelay: Int, ): CharacterRibbonRollAnimation { - val animation = remember(key) { + val animation = rememberSaveable( + key = key, + saver = CharacterRibbonRollAnimation.Saver, + ) { CharacterRibbonRollAnimation() } LaunchedEffect(key) { From 5632ec7c850ce3ac65f263a0620f8ca59fe393ab Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Mon, 12 May 2025 11:06:48 +0200 Subject: [PATCH 02/10] Add a filter button on the player inventory. --- .../drawable/ic_filter_list_24dp.xml | 9 ++ .../inventory/CharacterDetailInventory.kt | 79 ++++++++++---- .../CharacterDetailInventoryFactory.kt | 5 + .../detail/inventory/item/InventoryPurse.kt | 100 ++++++++---------- 4 files changed, 114 insertions(+), 79 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_filter_list_24dp.xml diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_filter_list_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_filter_list_24dp.xml new file mode 100644 index 0000000..621e515 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_filter_list_24dp.xml @@ -0,0 +1,9 @@ + + + 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 96d89d3..714593f 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 @@ -1,16 +1,23 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height 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.foundation.shape.CircleShape @@ -22,6 +29,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.AddCircle import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State @@ -32,6 +40,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp @@ -53,11 +62,13 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.item import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors import com.pixelized.desktop.lwa.ui.theme.lwa +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character__inventory__add_to_inventory__action import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp +import lwacharactersheet.composeapp.generated.resources.ic_filter_list_24dp import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -66,6 +77,7 @@ import org.koin.compose.viewmodel.koinViewModel data class CharacterDetailInventoryUio( val characterSheetId: String, val addItemAction: StateFlow, + val showFilter: StateFlow, val filter: LwaTextFieldUio, val purse: PurseUio, val items: List, @@ -292,35 +304,56 @@ private fun CharacterDetailInventoryContent( horizontalArrangement = Arrangement.spacedBy(space = spacing), ) { InventoryPurse( - modifier = Modifier.weight(weight = 1f), purse = inventory.purse, onPurse = { onPurse(inventory.characterSheetId) }, ) - LwaTextField( - modifier = Modifier.weight(weight = 1f), - colors = LwaTextFieldColors( - backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base2dp, - ), - field = inventory.filter, - trailingIcon = { - val value = inventory.filter.valueFlow.collectAsState() - AnimatedVisibility( - visible = value.value.isNotBlank(), - enter = fadeIn(), - exit = fadeOut(), - ) { - IconButton( - onClick = { inventory.filter.onValueChange.invoke("") }, - ) { - Icon( - painter = painterResource(Res.drawable.ic_cancel_24dp), - tint = MaterialTheme.lwa.colorScheme.base.primary, - contentDescription = null, - ) + Spacer( + modifier = Modifier.weight(weight = 1f) + ) + AnimatedContent( + targetState = inventory.showFilter.collectAsState().value, + ) { + when (it) { + true -> LwaTextField( + colors = LwaTextFieldColors( + backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base2dp, + ), + field = inventory.filter, + trailingIcon = { + val value = inventory.filter.valueFlow.collectAsState() + androidx.compose.animation.AnimatedVisibility( + visible = value.value.isNotBlank(), + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton( + onClick = { inventory.filter.onValueChange.invoke("") }, + ) { + Icon( + painter = painterResource(Res.drawable.ic_cancel_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + } + } } + ) + + else -> Box( + modifier = Modifier + .clip(shape = MaterialTheme.lwa.shapes.base.small) + .clickable { (inventory.showFilter as? MutableStateFlow)?.value = true } + .size(size = 32.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(Res.drawable.ic_filter_list_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null + ) } } - ) + } } } items( 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 cdf640a..8fe8beb 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 @@ -13,6 +13,7 @@ import com.pixelized.shared.lwa.model.inventory.Inventory import com.pixelized.shared.lwa.model.item.Item import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -45,6 +46,7 @@ class CharacterDetailInventoryFactory( addItemAction: StateFlow, initialValue: () -> CharacterDetailInventoryUio?, ): StateFlow { + val showFilterFlow = MutableStateFlow(false) val filterFlow = createLwaTextFieldFlow( label = getString(Res.string.character__inventory__filter_inventory__label), value = "", @@ -56,6 +58,7 @@ class CharacterDetailInventoryFactory( ) { inventory, items, filter -> convertToCharacterInventoryUio( characterSheetId = characterSheetId, + showFilter = showFilterFlow, filter = filterFlow.createLwaTextField(), addItemAction = addItemAction, purse = inventory.purse, @@ -71,6 +74,7 @@ class CharacterDetailInventoryFactory( private fun convertToCharacterInventoryUio( characterSheetId: String?, + showFilter: StateFlow, filter: LwaTextFieldUio, addItemAction: StateFlow, purse: Inventory.Purse?, @@ -86,6 +90,7 @@ class CharacterDetailInventoryFactory( silver = purse?.silver ?: 0, copper = purse?.copper ?: 0, ), + showFilter = showFilter, filter = filter, addItemAction = addItemAction, items = inventory diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryPurse.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryPurse.kt index da0c79f..3cde458 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryPurse.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/inventory/item/InventoryPurse.kt @@ -10,13 +10,11 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image 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.layout.size import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -29,11 +27,6 @@ import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.composable.currency.Currency import com.pixelized.desktop.lwa.ui.composable.currency.CurrencyUio import com.pixelized.desktop.lwa.ui.theme.lwa -import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.ic_copper_32px -import lwacharactersheet.composeapp.generated.resources.ic_gold_32px -import lwacharactersheet.composeapp.generated.resources.ic_silver_32px -import org.jetbrains.compose.resources.painterResource @Stable data class PurseUio( @@ -57,63 +50,58 @@ fun InventoryPurse( onPurse: () -> Unit, ) { Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, + modifier = Modifier + .clip(shape = MaterialTheme.lwa.shapes.item) + .clickable { onPurse() } + .padding(paddingValues = paddings) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), ) { - Row( - modifier = Modifier - .clip(shape = MaterialTheme.lwa.shapes.item) - .clickable { onPurse() } - .padding(paddingValues = paddings), - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - ) { - Row(verticalAlignment = Alignment.Bottom) { - Currency( - currency = CurrencyUio.Gold, + Row(verticalAlignment = Alignment.Bottom) { + Currency( + currency = CurrencyUio.Gold, + ) + AnimatedContent( + targetState = purse.gold, + transitionSpec = coinTransitionSpec(), + ) { + Text( + style = MaterialTheme.lwa.typography.system.body1, + fontWeight = FontWeight.Bold, + text = "$it", ) - AnimatedContent( - targetState = purse.gold, - transitionSpec = coinTransitionSpec(), - ) { - Text( - style = MaterialTheme.lwa.typography.system.body1, - fontWeight = FontWeight.Bold, - text = "$it", - ) - } } + } - Row(verticalAlignment = Alignment.Bottom) { - Currency( - currency = CurrencyUio.Silver, + Row(verticalAlignment = Alignment.Bottom) { + Currency( + currency = CurrencyUio.Silver, + ) + AnimatedContent( + targetState = purse.silver, + transitionSpec = coinTransitionSpec(), + ) { + Text( + style = MaterialTheme.lwa.typography.system.body1, + fontWeight = FontWeight.Bold, + text = "$it", ) - AnimatedContent( - targetState = purse.silver, - transitionSpec = coinTransitionSpec(), - ) { - Text( - style = MaterialTheme.lwa.typography.system.body1, - fontWeight = FontWeight.Bold, - text = "$it", - ) - } } + } - Row(verticalAlignment = Alignment.Bottom) { - Currency( - currency = CurrencyUio.Copper, + Row(verticalAlignment = Alignment.Bottom) { + Currency( + currency = CurrencyUio.Copper, + ) + AnimatedContent( + targetState = purse.copper, + transitionSpec = coinTransitionSpec(), + ) { + Text( + style = MaterialTheme.lwa.typography.system.body1, + fontWeight = FontWeight.Bold, + text = "$it", ) - AnimatedContent( - targetState = purse.copper, - transitionSpec = coinTransitionSpec(), - ) { - Text( - style = MaterialTheme.lwa.typography.system.body1, - fontWeight = FontWeight.Bold, - text = "$it", - ) - } } } } From a84c170396f1b5810519da8a2c3e1c57c5356772 Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Tue, 13 May 2025 09:48:51 +0200 Subject: [PATCH 03/10] Optimize server dispatcher --- server/src/main/kotlin/Module.kt | 5 + .../lwa/model/alteration/AlterationService.kt | 2 +- .../lwa/model/alteration/AlterationStore.kt | 181 +++++++------- .../lwa/model/campaign/CampaignService.kt | 14 +- .../lwa/model/campaign/CampaignStore.kt | 104 ++++---- .../model/character/CharacterSheetService.kt | 12 +- .../model/character/CharacterSheetStore.kt | 193 +++++++-------- .../lwa/model/inventory/InventoryService.kt | 16 +- .../lwa/model/inventory/InventoryStore.kt | 166 ++++++------- .../server/lwa/model/item/ItemService.kt | 4 +- .../server/lwa/model/item/ItemStore.kt | 228 +++++++++--------- .../server/lwa/model/tag/TagStore.kt | 17 +- 12 files changed, 483 insertions(+), 459 deletions(-) diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index e2ca61a..2b0345d 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -12,6 +12,8 @@ import com.pixelized.server.lwa.model.tag.TagService import com.pixelized.server.lwa.model.tag.TagStore import com.pixelized.server.lwa.server.Engine import com.pixelized.shared.lwa.utils.PathProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import org.koin.core.module.dsl.createdAtStart import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -29,6 +31,9 @@ val toolsDependencies single { PathProvider(appName = "LwaServer") } + factory { + CoroutineScope(Job()) + } } val engineDependencies diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt index e450290..91bb091 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt @@ -32,7 +32,7 @@ class AlterationService( } @Throws - fun save( + suspend fun save( json: AlterationJson, create: Boolean, ) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt index 58a1113..2414213 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt @@ -12,11 +12,11 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File import java.text.Collator @@ -24,15 +24,14 @@ import java.text.Collator class AlterationStore( private val pathProvider: PathProvider, private val factory: AlterationJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() } private val alterationFlow = MutableStateFlow>(emptyList()) init { - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateAlterationFlow() @@ -41,7 +40,7 @@ class AlterationStore( fun alterationsFlow(): StateFlow> = alterationFlow - fun updateAlterationFlow() { + suspend fun updateAlterationFlow() { alterationFlow.value = try { load() } catch (exception: Exception) { @@ -52,104 +51,110 @@ class AlterationStore( @Throws( FileReadException::class, + JsonCodingException::class, JsonConversionException::class, ) - private fun load( + private suspend fun load( directory: File = this.directory, ): List { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .listFiles() + ?.mapNotNull { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no alteration have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + val alterations = try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + return@mapNotNull alterations } - // Guard, if the json is blank no alteration have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - } - ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) - ?: emptyList() + ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) + ?: emptyList() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( json: AlterationJson, create: Boolean, ) { - val file = alterationFile(id = json.id) - // Guard case on update alteration - if (create && file.exists()) { - throw BusinessException( - message = "Alteration already exist, creation is impossible.", - ) - } - // Transform the json into the model. - val alteration = try { - factory.convertFromJson(json) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - if (alteration.id.isEmpty()) { - throw BusinessException( - message = "Alteration 'id' is a mandatory field.", - code = APIResponse.ErrorCode.AlterationId, - ) - } - if (alteration.metadata.name.isEmpty()) { - throw BusinessException( - message = "Alteration 'name' is a mandatory field.", - code = APIResponse.ErrorCode.AlterationName, - ) - } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Write the alteration into a file. - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - alterationFlow.update { alterations -> - val index = alterations.indexOfFirst { it.id == json.id } - alterations.toMutableList() - .also { - if (index >= 0) { - it[index] = alteration - } else { - it.add(alteration) + withContext(Dispatchers.IO) { + val file = alterationFile(id = json.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException( + message = "Alteration already exist, creation is impossible.", + ) + } + // Transform the json into the model. + val alteration = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + if (alteration.id.isEmpty()) { + throw BusinessException( + message = "Alteration 'id' is a mandatory field.", + code = APIResponse.ErrorCode.AlterationId, + ) + } + if (alteration.metadata.name.isEmpty()) { + throw BusinessException( + message = "Alteration 'name' is a mandatory field.", + code = APIResponse.ErrorCode.AlterationName, + ) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the alteration into a file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + alterationFlow.update { alterations -> + val index = alterations.indexOfFirst { it.id == json.id } + alterations.toMutableList() + .also { + if (index >= 0) { + it[index] = alteration + } else { + it.add(alteration) + } } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.name - }) + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.name + }) + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt index e71b8fa..7521cac 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt @@ -39,7 +39,7 @@ class CampaignService( } @Throws - fun addCharacter( + suspend fun addCharacter( characterSheetId: String, ) { // Check if the character is already in the campaign. @@ -57,7 +57,7 @@ class CampaignService( } @Throws - fun addNpc( + suspend fun addNpc( characterSheetId: String, ) { // Check if the character is already in the campaign. @@ -75,7 +75,7 @@ class CampaignService( } @Throws - fun removeCharacter( + suspend fun removeCharacter( characterSheetId: String, ) { // Check if the character is in the campaign. @@ -93,7 +93,7 @@ class CampaignService( } @Throws - fun removeNpc( + suspend fun removeNpc( characterSheetId: String, ) { // Check if the character is in the campaign. @@ -111,7 +111,7 @@ class CampaignService( } @Throws - fun setScene( + suspend fun setScene( scene: Campaign.Scene, ) { // save the campaign to the disk + update the flow. @@ -122,7 +122,7 @@ class CampaignService( // Data manipulation through WebSocket. - fun updateToggleParty() { + suspend fun updateToggleParty() { store.save( campaign = campaign.copy( options = campaign.options.copy( @@ -132,7 +132,7 @@ class CampaignService( ) } - fun updateToggleNpc() { + suspend fun updateToggleNpc() { store.save( campaign = campaign.copy( options = campaign.options.copy( diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt index f612cea..63bc43d 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt @@ -15,21 +15,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File class CampaignStore( private val pathProvider: PathProvider, private val factory: CampaignJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val campaignFlow = MutableStateFlow(value = Campaign.empty()) init { // create the directory if needed. File(pathProvider.campaignPath()).also { it.mkdirs() } - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateCampaignFlow() @@ -38,7 +38,7 @@ class CampaignStore( fun campaignFlow(): StateFlow = campaignFlow - fun updateCampaignFlow() { + suspend fun updateCampaignFlow() { campaignFlow.value = try { load() } catch (exception: Exception) { @@ -52,29 +52,31 @@ class CampaignStore( JsonCodingException::class, JsonConversionException::class, ) - fun load(): Campaign { - val file = campaignFile() - // Read the campaign file. - val data = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + suspend fun load(): Campaign { + return withContext(Dispatchers.IO) { + val file = campaignFile() + // Read the campaign file. + val data = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the file is empty we load a default campaign. + if (data.isBlank()) return@withContext Campaign.empty() + // Decode the json into a string. + val json = try { + jsonSerializer.decodeFromString(data) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Convert from the Json format + val campaign = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + return@withContext campaign } - // Guard, if the file is empty we load a default campaign. - if (data.isBlank()) return Campaign.empty() - // Decode the json into a string. - val json = try { - this.json.decodeFromString(data) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Convert from the Json format - val campaign = try { - factory.convertFromJson(json) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - return campaign } @Throws( @@ -82,31 +84,33 @@ class CampaignStore( JsonCodingException::class, FileWriteException::class, ) - fun save(campaign: Campaign) { - // Transform the json into the model. - val json = try { - factory.convertToJson(campaign = campaign) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) + suspend fun save(campaign: Campaign) { + withContext(Dispatchers.IO) { + // Transform the json into the model. + val json = try { + factory.convertToJson(campaign = campaign) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the file + try { + val file = campaignFile() + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + campaignFlow.update { campaign } } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Write the file - try { - val file = campaignFile() - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - campaignFlow.update { campaign } } private fun campaignFile(): File { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt index 5005af7..a02611e 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt @@ -55,7 +55,7 @@ class CharacterSheetService( } @Throws - fun deleteCharacterSheet( + suspend fun deleteCharacterSheet( characterSheetId: String, ) { characterStore.delete( @@ -65,7 +65,7 @@ class CharacterSheetService( // Data manipulation through WebSocket. - fun updateAlteration( + suspend fun updateAlteration( characterSheetId: String, alterationId: String, active: Boolean, @@ -97,7 +97,7 @@ class CharacterSheetService( } } - fun updateDamage( + suspend fun updateDamage( characterSheetId: String, damage: Int, ) { @@ -110,7 +110,7 @@ class CharacterSheetService( } } - fun updateDiminished( + suspend fun updateDiminished( characterSheetId: String, diminished: Int, ) { @@ -123,7 +123,7 @@ class CharacterSheetService( } } - fun updateFatigue( + suspend fun updateFatigue( characterSheetId: String, fatigue: Int, ) { @@ -136,7 +136,7 @@ class CharacterSheetService( } } - fun updateCharacterSkillUsage( + suspend fun updateCharacterSkillUsage( characterSheetId: String, skillId: String, used: Boolean, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt index f2b3545..3cd0b3d 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt @@ -12,11 +12,11 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File import java.text.Collator @@ -24,14 +24,13 @@ import java.text.Collator class CharacterSheetStore( private val pathProvider: PathProvider, private val factory: CharacterSheetJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.characterStorePath()).also { it.mkdirs() } private val characterSheetsFlow = MutableStateFlow>(value = emptyList()) init { - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateCharacterFlow() @@ -40,7 +39,7 @@ class CharacterSheetStore( fun characterSheetsFlow(): StateFlow> = characterSheetsFlow - fun updateCharacterFlow() { + suspend fun updateCharacterFlow() { characterSheetsFlow.value = try { load() } catch (exception: Exception) { @@ -54,112 +53,118 @@ class CharacterSheetStore( JsonCodingException::class, JsonConversionException::class, ) - fun load( + suspend fun load( directory: File = this.directory, ): List { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .listFiles() + ?.mapNotNull { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no character have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } } - // Guard, if the json is blank no character have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - } - ?.sortedWith(compareBy(Collator.getInstance()) { it.name }) - ?: emptyList() + ?.sortedWith(compareBy(Collator.getInstance()) { it.name }) + ?: emptyList() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( sheet: CharacterSheet, create: Boolean, ) { - val file = characterSheetFile(id = sheet.id) - // Guard case on update alteration - if (create && file.exists()) { - throw BusinessException(message = "Character already exist, creation is impossible.") - } - // Transform the json into the model. - val json = try { - factory.convertToJson(sheet = sheet) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // write the character file. - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - characterSheetsFlow.update { characters -> - characters.toMutableList() - .also { data -> - val index = data.indexOfFirst { it.id == sheet.id } - if (index >= 0) { - data[index] = sheet - } else { - data.add(sheet) + withContext(Dispatchers.IO) { + val file = characterSheetFile(id = sheet.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException(message = "Character already exist, creation is impossible.") + } + // Transform the json into the model. + val json = try { + factory.convertToJson(sheet = sheet) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // write the character file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + characterSheetsFlow.update { characters -> + characters.toMutableList() + .also { data -> + val index = data.indexOfFirst { it.id == sheet.id } + if (index >= 0) { + data[index] = sheet + } else { + data.add(sheet) + } } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.name - }) + .sortedWith(compareBy(Collator.getInstance()) { + it.name + }) + } } } @Throws(BusinessException::class) - fun delete(characterSheetId: String) { - val file = characterSheetFile(id = characterSheetId) - // Guard case on the file existence. - if (file.exists().not()) { - throw BusinessException( - message = "Character file with id:$characterSheetId doesn't not exist.", - code = APIResponse.ErrorCode.CharacterSheetId - ) - } - // Guard case on the file deletion - if (file.delete().not()) { - throw BusinessException( - message = "Character file have not been deleted for unknown reason.", - ) - } - // Update the data model with the deleted character. - characterSheetsFlow.update { characters -> - characters.toMutableList() - .also { data -> data.removeIf { it.id == characterSheetId } } - .sortedWith(compareBy(Collator.getInstance()) { it.name }) + suspend fun delete(characterSheetId: String) { + withContext(Dispatchers.IO) { + val file = characterSheetFile(id = characterSheetId) + // Guard case on the file existence. + if (file.exists().not()) { + throw BusinessException( + message = "Character file with id:$characterSheetId doesn't not exist.", + code = APIResponse.ErrorCode.CharacterSheetId + ) + } + // Guard case on the file deletion + if (file.delete().not()) { + throw BusinessException( + message = "Character file have not been deleted for unknown reason.", + ) + } + // Update the data model with the deleted character. + characterSheetsFlow.update { characters -> + characters.toMutableList() + .also { data -> data.removeIf { it.id == characterSheetId } } + .sortedWith(compareBy(Collator.getInstance()) { it.name }) + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt index 5aa75d4..9c0ba11 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt @@ -40,7 +40,7 @@ class InventoryService( } @Throws - fun updatePurse( + suspend fun updatePurse( purse: ApiPurseJson, ) { val inventory = inventory( @@ -59,7 +59,7 @@ class InventoryService( } @Throws - fun save( + suspend fun save( inventoryJson: InventoryJson, create: Boolean, ) { @@ -70,12 +70,12 @@ class InventoryService( } @Throws - fun delete(characterSheetId: String) { + suspend fun delete(characterSheetId: String) { inventoryStore.delete(characterSheetId = characterSheetId) } @Throws - fun createInventoryItem( + suspend fun createInventoryItem( characterSheetId: String, itemId: String, count: Float, @@ -107,7 +107,7 @@ class InventoryService( } @Throws - fun changeInventoryItemCount( + suspend fun changeInventoryItemCount( characterSheetId: String, inventoryId: String, count: Float, @@ -145,7 +145,7 @@ class InventoryService( } @Throws - fun consumeInventoryItem( + suspend fun consumeInventoryItem( characterSheetId: String, inventoryId: String, ) { @@ -185,7 +185,7 @@ class InventoryService( } @Throws - fun equipInventoryItem( + suspend fun equipInventoryItem( characterSheetId: String, inventoryId: String, equip: Boolean, @@ -223,7 +223,7 @@ class InventoryService( } @Throws - fun deleteInventoryItem( + suspend fun deleteInventoryItem( characterSheetId: String, inventoryId: String, ) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt index b9f3fed..4c8f32a 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt @@ -12,26 +12,24 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File -import java.util.UUID class InventoryStore( private val pathProvider: PathProvider, private val factory: InventoryJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.inventoryPath()).also { it.mkdirs() } private val inventoryFlow = MutableStateFlow>(value = emptyMap()) init { - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateInventoryFlow() @@ -40,7 +38,7 @@ class InventoryStore( fun inventoryFlow(): StateFlow> = inventoryFlow - fun updateInventoryFlow() { + suspend fun updateInventoryFlow() { inventoryFlow.value = try { load() } catch (exception: Exception) { @@ -54,102 +52,104 @@ class InventoryStore( JsonCodingException::class, JsonConversionException::class, ) - private fun load( + private suspend fun load( directory: File = this.directory, ): Map { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .listFiles() + ?.mapNotNull { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no character have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + val inventory = try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + inventory.characterSheetId to inventory } - // Guard, if the json is blank no character have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - val inventory = try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - inventory.characterSheetId to inventory - } - ?.toMap() - ?: emptyMap() + ?.toMap() + ?: emptyMap() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( inventory: Inventory, create: Boolean, ) { - val file = inventoryFile(id = inventory.characterSheetId) - - if (create && file.exists()) { - throw BusinessException(message = "Inventory already exist, creation is impossible.") - } - - val json = try { - factory.convertToJson(inventory = inventory) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, + withContext(Dispatchers.IO) { + val file = inventoryFile( + id = inventory.characterSheetId ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - - inventoryFlow.update { flow -> - flow.toMutableMap().also { data -> - data[inventory.characterSheetId] = inventory + if (create && file.exists()) { + throw BusinessException(message = "Inventory already exist, creation is impossible.") + } + val json = try { + factory.convertToJson(inventory = inventory) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + inventoryFlow.update { flow -> + flow.toMutableMap().also { data -> + data[inventory.characterSheetId] = inventory + } } } } @Throws(BusinessException::class) - fun delete(characterSheetId: String) { - val file = inventoryFile(id = characterSheetId) - - if (file.exists().not()) { - throw BusinessException( - message = "Inventory file with id:$characterSheetId doesn't not exist.", - code = APIResponse.ErrorCode.CharacterSheetId + suspend fun delete(characterSheetId: String) { + withContext(Dispatchers.IO) { + val file = inventoryFile( + id = characterSheetId, ) - } - - if (file.delete().not()) { - throw BusinessException( - message = "Inventory file have not been deleted for unknown reason.", - ) - } - - inventoryFlow.update { characters -> - characters.toMutableMap().also { data -> data.remove(characterSheetId) } + if (file.exists().not()) { + throw BusinessException( + message = "Inventory file with id:$characterSheetId doesn't not exist.", + code = APIResponse.ErrorCode.CharacterSheetId + ) + } + if (file.delete().not()) { + throw BusinessException( + message = "Inventory file have not been deleted for unknown reason.", + ) + } + inventoryFlow.update { characters -> + characters.toMutableMap().also { data -> data.remove(characterSheetId) } + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt index b3d1aba..ed03b38 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt @@ -32,7 +32,7 @@ class ItemService( } @Throws - fun save( + suspend fun save( json: ItemJson, create: Boolean, ) { @@ -43,7 +43,7 @@ class ItemService( } @Throws - fun delete(itemId: String) { + suspend fun delete(itemId: String) { itemStore.delete(id = itemId) } } 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 cfea1ef..135977a 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 @@ -12,11 +12,11 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File import java.text.Collator @@ -24,14 +24,13 @@ import java.text.Collator class ItemStore( private val pathProvider: PathProvider, private val factory: ItemJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.itemsPath()).also { it.mkdirs() } private val itemFlow = MutableStateFlow>(emptyList()) init { - val scope = CoroutineScope(Dispatchers.IO + Job()) - scope.launch { updateItemsFlow() } @@ -43,7 +42,7 @@ class ItemStore( fun itemsFlow(): StateFlow> = itemFlow - fun updateItemsFlow() { + suspend fun updateItemsFlow() { itemFlow.value = try { load() } catch (exception: Exception) { @@ -54,131 +53,138 @@ class ItemStore( @Throws( FileReadException::class, + JsonCodingException::class, JsonConversionException::class, ) - private fun load( + private suspend fun load( directory: File = this.directory, ): List { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .listFiles() + ?.mapNotNull { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no item have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } } - // Guard, if the json is blank no item have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - } - ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.label }) - ?: emptyList() + ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.label }) + ?: emptyList() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( json: ItemJson, create: Boolean, ) { - val file = itemFile(id = json.id) - // Guard case on update alteration - if (create && file.exists()) { - throw BusinessException( - message = "Item already exist, creation is impossible.", - ) - } - // Transform the json into the model. - val item = try { - factory.convertFromJson(json) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - if (item.id.isEmpty()) { - throw BusinessException( - message = "Item 'id' is a mandatory field.", - code = APIResponse.ErrorCode.ItemId, - ) - } - if (item.metadata.label.isEmpty()) { - throw BusinessException( - message = "Item 'name' is a mandatory field.", - code = APIResponse.ErrorCode.ItemName, - ) - } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Write the alteration into a file. - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - itemFlow.update { items -> - val index = items.indexOfFirst { it.id == json.id } - items.toMutableList() - .also { - if (index >= 0) { - it[index] = item - } else { - it.add(item) + withContext(Dispatchers.IO) { + val file = itemFile(id = json.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException( + message = "Item already exist, creation is impossible.", + ) + } + // Transform the json into the model. + val item = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + if (item.id.isEmpty()) { + throw BusinessException( + message = "Item 'id' is a mandatory field.", + code = APIResponse.ErrorCode.ItemId, + ) + } + if (item.metadata.label.isEmpty()) { + throw BusinessException( + message = "Item 'name' is a mandatory field.", + code = APIResponse.ErrorCode.ItemName, + ) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the alteration into a file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + itemFlow.update { items -> + val index = items.indexOfFirst { it.id == json.id } + items.toMutableList() + .also { + if (index >= 0) { + it[index] = item + } else { + it.add(item) + } } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.label - }) + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.label + }) + } } } @Throws(BusinessException::class) - fun delete(id: String) { - val file = itemFile(id = id) - // Guard case on the file existence. - if (file.exists().not()) { - throw BusinessException( - message = "Item doesn't not exist, deletion is impossible.", - ) - } - // Guard case on the file deletion - if (file.delete().not()) { - throw BusinessException( - message = "Item file have not been deleted for unknown reason.", - ) - } - // Update the data model with the deleted alteration. - itemFlow.update { items -> - items.toMutableList() - .also { item -> - item.removeIf { it.id == id } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.label - }) + suspend fun delete(id: String) { + withContext(Dispatchers.IO) { + val file = itemFile(id = id) + // Guard case on the file existence. + if (file.exists().not()) { + throw BusinessException( + message = "Item doesn't not exist, deletion is impossible.", + ) + } + // Guard case on the file deletion + if (file.delete().not()) { + throw BusinessException( + message = "Item file have not been deleted for unknown reason.", + ) + } + // Update the data model with the deleted alteration. + itemFlow.update { items -> + items.toMutableList() + .also { item -> + item.removeIf { it.id == id } + } + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.label + }) + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt index b87d48d..4fd02b7 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt @@ -20,7 +20,8 @@ private const val ITEM = "item" class TagStore( private val pathProvider: PathProvider, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val alterationTagsFlow = MutableStateFlow>(emptyMap()) private val characterTagsFlow = MutableStateFlow>(emptyMap()) @@ -29,8 +30,6 @@ class TagStore( init { // make the file path. File(pathProvider.tagsPath()).mkdirs() - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateTagFlow() @@ -41,7 +40,7 @@ class TagStore( fun characterTags(): StateFlow> = characterTagsFlow fun itemTags(): StateFlow> = itemTagsFlow - fun updateTagFlow() { + suspend fun updateTagFlow() { update( flow = alterationTagsFlow, file = alterationFile(), @@ -56,7 +55,7 @@ class TagStore( ) } - private fun update( + private suspend fun update( flow: MutableStateFlow>, file: File, ) { @@ -69,7 +68,7 @@ class TagStore( } @Throws(FileReadException::class, JsonConversionException::class) - private fun File.readTags(): List { + private suspend fun File.readTags(): List { // read the file (force the UTF8 format) val data = try { readText(charset = Charsets.UTF_8) @@ -81,7 +80,7 @@ class TagStore( return emptyList() } return try { - json.decodeFromString>(data) + jsonSerializer.decodeFromString>(data) } catch (exception: Exception) { throw JsonConversionException( root = exception @@ -90,10 +89,10 @@ class TagStore( } @Throws(JsonConversionException::class, FileWriteException::class) - private fun saveAlterationTags(tags: List) { + private suspend fun saveAlterationTags(tags: List) { // convert the data to json format val json = try { - this.json.encodeToString(tags) + this.jsonSerializer.encodeToString(tags) } catch (exception: Exception) { throw JsonConversionException(root = exception) } From 50697ceb93e2d97d2aa82b0a5f1fe3574982bbb2 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sat, 17 May 2025 23:10:27 +0200 Subject: [PATCH 04/10] Fix portrait display to use the preview instead of the character Detail. This avoid to fetch the character detail just to get the portrait. --- .../composeResources/values/strings.xml | 3 ++ .../CharacterSheetRepository.kt | 4 +- .../ui/overlay/portrait/PortraitOverlay.kt | 39 ++++++++++++++++--- .../portrait/PortraitOverlayViewModel.kt | 10 +++-- .../campaign/text/TextMessageFactory.kt | 4 +- .../desktop/lwa/ui/theme/size/LwaSize.kt | 2 - .../characterSheet/CharacterSheetPreview.kt | 2 + .../factory/CharacterSheetJsonFactory.kt | 4 ++ 8 files changed, 53 insertions(+), 15 deletions(-) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 0de75f5..164a914 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -284,6 +284,9 @@ Passage du niveau %1$d ▸ %2$d niv : %1$d - + Lien vers l'image + Fermer + Admin GameMaster Sauvegarder diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt index 74b9ec4..9e7ab9a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt @@ -27,8 +27,8 @@ class CharacterSheetRepository( store.updateCharacterSheetDetailFlow(characterSheetId = characterSheetId) } - fun characterPreview(characterId: String?): CharacterSheetPreview? { - return store.previewFlow.value.firstOrNull { it.characterSheetId == characterId } + fun characterPreview(characterSheetId: String?): CharacterSheetPreview? { + return store.previewFlow.value.firstOrNull { it.characterSheetId == characterSheetId } } fun characterDetail( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlay.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlay.kt index 39586d3..4d4f2ce 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlay.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlay.kt @@ -6,12 +6,18 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State @@ -21,13 +27,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp import lwacharactersheet.composeapp.generated.resources.ic_link_24dp +import lwacharactersheet.composeapp.generated.resources.portrait_overlay__close +import lwacharactersheet.composeapp.generated.resources.portrait_overlay__external_link import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Stable @@ -79,30 +91,35 @@ private fun PortraitContent( ) { when (it) { null -> Box( - modifier = Modifier.size(size = MaterialTheme.lwa.size.portrait.maximized) + modifier = Modifier.fillMaxSize(), ) else -> Box( - modifier = Modifier.size(size = MaterialTheme.lwa.size.portrait.maximized) + modifier = Modifier.fillMaxSize(), ) { AsyncImage( modifier = Modifier.matchParentSize(), model = it, filterQuality = FilterQuality.High, + contentScale = ContentScale.Inside, + alignment = Alignment.Center, contentDescription = null ) Row( modifier = Modifier - .align(alignment = Alignment.TopEnd) + .align(alignment = Alignment.BottomCenter) .animateContentSize(), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), ) { AnimatedVisibility( visible = options.value.isBrowserAvailable, enter = fadeIn(), exit = fadeOut(), ) { - IconButton( + Button( + colors = LwaButtonColors(), + shape = CircleShape, onClick = { onDownload(it) }, ) { Icon( @@ -110,6 +127,10 @@ private fun PortraitContent( tint = MaterialTheme.lwa.colorScheme.base.primary, contentDescription = null ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = stringResource(Res.string.portrait_overlay__external_link), + ) } } AnimatedVisibility( @@ -117,7 +138,9 @@ private fun PortraitContent( enter = fadeIn(), exit = fadeOut(), ) { - IconButton( + Button( + colors = LwaButtonColors(), + shape = CircleShape, onClick = onClose, ) { Icon( @@ -125,6 +148,10 @@ private fun PortraitContent( tint = MaterialTheme.lwa.colorScheme.base.primary, contentDescription = null ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = stringResource(Res.string.portrait_overlay__close), + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlayViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlayViewModel.kt index e5e75b4..8ced0d1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlayViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/portrait/PortraitOverlayViewModel.kt @@ -25,9 +25,13 @@ class PortraitOverlayViewModel( @OptIn(ExperimentalCoroutinesApi::class) val portrait = networkRepository.data .mapNotNull { it as? GameMasterEvent.DisplayPortrait } - .flatMapLatest { characterSheetRepository.characterDetailFlow(characterSheetId = it.characterSheetId) } - .map { it?.portrait } + .flatMapLatest { portrait -> + characterSheetRepository.characterSheetPreviewFlow().map { previews -> + previews.firstOrNull { it.characterSheetId == portrait.characterSheetId } + } + } .distinctUntilChanged() + .map { it?.portrait } .stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, @@ -37,7 +41,7 @@ class PortraitOverlayViewModel( val options = settingsRepository.settingsFlow() .map { settings -> PortraitOptionUio( - isGameMaster = settings.isGameMaster ?: false, + isGameMaster = settings.isGameMaster == true, isBrowserAvailable = Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE) ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt index 0148b95..cbb6ece 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt @@ -38,7 +38,7 @@ class TextMessageFactory( return when (message) { is RollEvent -> { val sheetPreview = characterSheetRepository - .characterPreview(characterId = message.characterSheetId) + .characterPreview(characterSheetId = message.characterSheetId) ?: return null val isGm = settings.isGameMaster ?: false @@ -72,7 +72,7 @@ class TextMessageFactory( if ((isInParty || isInNpcs).not()) return null // get the character sheet val sheetPreview = characterSheetRepository - .characterPreview(characterId = message.characterSheetId) + .characterPreview(characterSheetId = message.characterSheetId) ?: return null DiminishedTextMessageUio( 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 9650cc7..57dd396 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 @@ -14,7 +14,6 @@ data class LwaSize( ) { @Stable data class Portrait( - val maximized: DpSize, val minimized: DpSize, ) @@ -30,7 +29,6 @@ data class LwaSize( fun lwaSize( portrait: LwaSize.Portrait = LwaSize.Portrait( minimized = DpSize(width = 96.dp, height = 128.dp), - maximized = DpSize(width = 512.dp, height = 512.dp), ), sheet: LwaSize.Sheet = LwaSize.Sheet( subCategory = 14.dp, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetPreview.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetPreview.kt index 3c2c27c..eb4a32a 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetPreview.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetPreview.kt @@ -3,6 +3,8 @@ package com.pixelized.shared.lwa.model.characterSheet data class CharacterSheetPreview( val characterSheetId: String, val name: String, + val portrait: String?, + val thumbnail: String?, val job: String, val level: Int, val externalLink: String?, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt index 40bb456..400e53a 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/factory/CharacterSheetJsonFactory.kt @@ -101,6 +101,8 @@ class CharacterSheetJsonFactory( return CharacterSheetPreview( characterSheetId = sheet.id, name = sheet.name, + portrait = sheet.portrait, + thumbnail = sheet.thumbnail, job = sheet.job, level = sheet.level, externalLink = sheet.externalLink, @@ -114,6 +116,8 @@ class CharacterSheetJsonFactory( return CharacterSheetPreview( characterSheetId = json.id, name = json.name, + portrait = json.portrait, + thumbnail = json.thumbnail, job = json.job ?: "", level = json.level, externalLink = json.externalLink, From 17b7b06ec8320afe1b8867dee3e6cfc19519edf7 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Mon, 19 May 2025 17:24:36 +0200 Subject: [PATCH 05/10] Bump version to 1.5.0 --- composeApp/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 65947ed..ab0b8f3 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -9,7 +9,7 @@ plugins { alias(libs.plugins.buildkonfig) } -fun getVersion() = "1.4.0" +fun getVersion() = "1.5.0" kotlin { jvm("desktop") From 51a477bb582471dea8b556e5ba6f8784ccdb90e2 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Tue, 20 May 2025 12:50:09 +0200 Subject: [PATCH 06/10] Add alteration support for diminished status. --- .../CharacterSheetDiminishedDialog.kt | 52 +++++++++++++------ .../CharacterSheetDiminishedDialogFactory.kt | 20 +++---- .../shared/lwa/model/AlteredCharacterSheet.kt | 9 ++-- .../model/characterSheet/CharacterSheet.kt | 4 ++ .../shared/lwa/utils/IntEx+signLabel.kt | 10 ++++ 5 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/IntEx+signLabel.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt index 9ca3d7b..b1ed627 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialog.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor @@ -45,6 +46,7 @@ data class CharacterSheetDiminishedDialogUio( val label: String, val value: () -> TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, + val additional: String?, ) @Composable @@ -76,6 +78,14 @@ private fun CharacterSheetDiminishedContent( val typography = MaterialTheme.typography val colors = MaterialTheme.colors + val valueTypography = remember { + typography.h5.copy( + color = colors.primary, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + } + val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -107,24 +117,32 @@ private fun CharacterSheetDiminishedContent( style = MaterialTheme.typography.caption, text = dialog.label, ) - BasicTextField( - modifier = Modifier - .focusRequester(focusRequester = focusRequester) - .width(width = 120.dp), - textStyle = remember { - typography.h5.copy( - color = colors.primary, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold + Row( + horizontalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.CenterHorizontally + ) + ) { + BasicTextField( + modifier = Modifier + .focusRequester(focusRequester = focusRequester) + .width(width = 120.dp), + textStyle = valueTypography, + cursorBrush = SolidColor(MaterialTheme.colors.primary), + singleLine = true, + keyboardActions = KeyboardActions { onConfirm(dialog) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + value = dialog.value(), + onValueChange = dialog.onValueChange, + ) + dialog.additional?.let { + Text( + modifier = Modifier.alpha(alpha = 0.66f), + style = valueTypography, + text = it ) - }, - cursorBrush = SolidColor(MaterialTheme.colors.primary), - singleLine = true, - keyboardActions = KeyboardActions { onConfirm(dialog) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - value = dialog.value(), - onValueChange = dialog.onValueChange, - ) + } + } Row( modifier = Modifier .padding(top = 4.dp) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialogFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialogFactory.kt index 918ca5f..d4baf8c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialogFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/diminished/CharacterSheetDiminishedDialogFactory.kt @@ -6,11 +6,12 @@ import androidx.compose.ui.text.input.TextFieldValue import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory -import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet +import com.pixelized.shared.lwa.utils.signLabel import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label import org.jetbrains.compose.resources.getString +import kotlin.math.abs class CharacterSheetDiminishedDialogFactory( private val characterSheetRepository: CharacterSheetRepository, @@ -19,7 +20,7 @@ class CharacterSheetDiminishedDialogFactory( ) { suspend fun convertToDialogUio( characterSheetId: String?, - ) : CharacterSheetDiminishedDialogUio? { + ): CharacterSheetDiminishedDialogUio? { if (characterSheetId == null) return null @@ -29,21 +30,19 @@ class CharacterSheetDiminishedDialogFactory( if (characterSheet == null) return null - val alterations: Map> = alterationRepository.activeFieldAlterations( - characterSheetId = characterSheetId, - ) - val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( characterSheet = characterSheet, - alterations = alterations, + alterations = alterationRepository.activeFieldAlterations( + characterSheetId = characterSheetId + ), ) - val textFieldValue = mutableStateOf( TextFieldValue( - text = "${alteredCharacterSheet.diminished}", + text = "${characterSheet.diminished}", selection = TextRange(index = 0), ) ) + return CharacterSheetDiminishedDialogUio( characterSheetId = characterSheetId, label = getString(resource = Res.string.character_sheet__diminished__label), @@ -54,6 +53,9 @@ class CharacterSheetDiminishedDialogFactory( else -> value } }, + additional = (alteredCharacterSheet.diminishedAlterations) + .takeIf { it != 0 } + ?.let { "${it.signLabel}${abs(it)}" } ) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt index 3845259..85d5a32 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt @@ -21,11 +21,11 @@ import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.Characterist import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.REFLEX import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.THUMBNAIL -import com.pixelized.shared.lwa.model.inventory.Inventory -import com.pixelized.shared.lwa.model.inventory.Inventory.Item +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.StatusId.DIMINISHED import com.pixelized.shared.lwa.parser.expression.Expression import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import com.pixelized.shared.lwa.usecase.ExpressionUseCase +import kotlin.math.max class AlteredCharacterSheetFactory( private val sheetUseCase: CharacterSheetUseCase, @@ -109,7 +109,10 @@ class AlteredCharacterSheet( get() = sheet.fatigue val diminished: Int - get() = sheet.diminished + get() = max(0, sheet.diminished + diminishedAlterations) + + val diminishedAlterations: Int + get() = fieldAlterations[DIMINISHED].sum() val alterations: List get() = sheet.alterations diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt index d389595..3d5fe99 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt @@ -56,6 +56,10 @@ data class CharacterSheet( val critical: String?, ) + object StatusId { + const val DIMINISHED = "DIMINISHED" + } + object CharacteristicId { const val PORTRAIT = "PORTRAIT" const val THUMBNAIL = "THUMBNAIL" diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/IntEx+signLabel.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/IntEx+signLabel.kt new file mode 100644 index 0000000..7e36767 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/IntEx+signLabel.kt @@ -0,0 +1,10 @@ +package com.pixelized.shared.lwa.utils + +import kotlin.math.sign + +val Int.signLabel: Char + get() = when (this.sign) { + 1 -> '+' + -1 -> '-' + else -> ' ' + } \ No newline at end of file From 741bb7cf251bd1bea0b621f18523f9eede01fa36 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sun, 15 Jun 2025 13:23:34 +0200 Subject: [PATCH 07/10] Fix crash on null quantity when adding an item. --- .../lwa/ui/composable/character/item/ItemDetailDialogFactory.kt | 2 +- .../ui/composable/character/item/ItemDetailDialogViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 192e136..37d4286 100644 --- 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 @@ -65,6 +65,6 @@ class ItemDetailDialogFactory { inventoryId: String?, value: String, ): Boolean { - return floatChecker.matches(value).not() || (inventoryId == null && value.toFloat() < 1f) + return value.isBlank() || floatChecker.matches(value).not() || (inventoryId == null && value.toFloat() < 1f) } } \ 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 index ae95364..fb5c403 100644 --- 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 @@ -52,7 +52,7 @@ class ItemDetailDialogViewModel( characterSheetId = ids?.characterSheetId, items = items, count = selectedInventoryItem?.count ?: 1f, - equipped = selectedInventoryItem?.equipped ?: false, + equipped = selectedInventoryItem?.equipped == true, inventoryId = ids?.inventoryId, itemId = ids?.itemId, ) From 894d8db493d49b0e91678de094e1f736ea850bd3 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sun, 15 Jun 2025 14:51:40 +0200 Subject: [PATCH 08/10] Change the UI of damage & fatigue dialogs. --- .../composeResources/values/strings.xml | 2 + .../lwa/ui/composable/button/SignButton.kt | 58 ++++ .../CharacterSheetCharacteristicDialog.kt | 256 +++++++++--------- ...aracterSheetCharacteristicDialogFactory.kt | 82 ++++-- ...acterSheetCharacteristicDialogViewModel.kt | 55 ++-- .../composable/character/purse/PurseDialog.kt | 57 +--- .../composable/checkbox/LwaCheckBoxHelper.kt | 30 ++ .../textfield/LwaTextFieldHelper.kt | 6 +- .../lwa/ui/screen/campaign/CampaignScreen.kt | 6 +- .../campaign/text/TextMessageFactory.kt | 9 +- .../messages/CharacteristicTextMessage.kt | 3 +- .../character/list/GMCharacterPage.kt | 6 +- .../rest/character/PUT_Character_Damage.kt | 5 + .../shared/lwa/protocol/rest/APIResponse.kt | 1 + .../protocol/websocket/CharacterSheetEvent.kt | 1 + 15 files changed, 354 insertions(+), 223 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/button/SignButton.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/checkbox/LwaCheckBoxHelper.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 164a914..4f9d158 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -84,6 +84,7 @@ Points de vie maximum Points de vie Points de pouvoir maximum + Utiliser l'armure Points de pouvoir Bonus aux dégats Armure @@ -253,6 +254,7 @@ %1$s passe à %2$d d'état diminuée %1$s subit %2$d point(s) de dommage + %1$s subit %2$d point(s) de dommage (armure : %3$d) %1$s récupère %2$d point(s) de vie %1$s utilise %2$d point(s) de pouvoir %1$s récupère %2$d point(s) de pouvoir diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/button/SignButton.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/button/SignButton.kt new file mode 100644 index 0000000..5fab8d5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/button/SignButton.kt @@ -0,0 +1,58 @@ +package com.pixelized.desktop.lwa.ui.composable.button + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import com.pixelized.desktop.lwa.ui.theme.lwa +import kotlinx.coroutines.flow.StateFlow +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_remove_24dp +import org.jetbrains.compose.resources.painterResource + +@Composable +fun SignButton( + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource? = null, + enabled: Boolean = true, + add: StateFlow, + onClick: () -> Unit, +) { + IconButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + interactionSource = interactionSource, + ) { + val rotation = animateFloatAsState( + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + targetValue = when (add.collectAsState().value) { + true -> 90f + else -> 0f + } + ) + Icon( + modifier = Modifier.graphicsLayer { + this.rotationZ = rotation.value * 2f + }, + painter = painterResource(Res.drawable.ic_remove_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null + ) + Icon( + modifier = Modifier.graphicsLayer { + this.rotationZ = rotation.value * 3f + }, + painter = painterResource(Res.drawable.ic_remove_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt index e4c0dbe..c19946f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt @@ -1,40 +1,43 @@ package com.pixelized.desktop.lwa.ui.composable.character.characteristic -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.background 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.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import androidx.compose.ui.unit.DpSize 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.utils.extention.onPreviewEscape +import com.pixelized.desktop.lwa.ui.composable.button.SignButton +import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog +import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBox +import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio +import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.theme.lwa +import kotlinx.coroutines.flow.StateFlow import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__use_armor import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action import org.jetbrains.compose.resources.stringResource @@ -44,9 +47,10 @@ data class CharacterSheetCharacteristicDialogUio( val characterSheetId: String, val characteristic: Characteristic, val label: String, - val value: () -> TextFieldValue, - val onValueChange: (TextFieldValue) -> Unit, - val maxValue: String, + val add: StateFlow, + val value: LwaTextFieldUio, + val enableArmor: LwaCheckBoxUio?, + val enableConfirm: StateFlow, ) { @Stable enum class Characteristic { @@ -55,125 +59,131 @@ data class CharacterSheetCharacteristicDialogUio( } } -@Composable -fun CharacterSheetCharacteristicDialog( - dialog: State, - onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit, - onDismissRequest: () -> Unit, -) { - dialog.value?.let { - Dialog( - onDismissRequest = onDismissRequest, - content = { - CharacterSheetCharacteristicContent( - dialog = it, - onConfirm = onConfirm, - onDismissRequest = onDismissRequest, - ) - } - ) - } +@Stable +object CharacterSheetCharacteristicDialogDefault { + @Stable + val paddings: PaddingValues = + PaddingValues(top = 16.dp, start = 8.dp, end = 8.dp, bottom = 8.dp) + + @Stable + val spacings: DpSize = DpSize(width = 4.dp, height = 8.dp) } @Composable -private fun CharacterSheetCharacteristicContent( - dialog: CharacterSheetCharacteristicDialogUio, +fun CharacterSheetCharacteristicDialog( + dialog: State, + paddings: PaddingValues = CharacterSheetCharacteristicDialogDefault.paddings, + spacings: DpSize = CharacterSheetCharacteristicDialogDefault.spacings, onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit, + onSwapSign: (CharacterSheetCharacteristicDialogUio) -> Unit, onDismissRequest: () -> Unit, ) { - val typography = MaterialTheme.typography - val colors = MaterialTheme.colors + LwaDialog( + state = dialog, + onDismissRequest = onDismissRequest, + onConfirm = { dialog.value?.let(onConfirm) } + ) { state -> + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } + CharacterSheetCharacteristicDialogKeyHandler( + onSwap = { onSwapSign(state) }, + ) - Box( - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onDismissRequest, + Column( + modifier = Modifier.padding(paddingValues = paddings), + verticalArrangement = Arrangement.spacedBy(space = spacings.height), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + style = MaterialTheme.typography.caption, + text = state.label, ) - .onPreviewEscape( - escape = onDismissRequest, - enter = { onConfirm(dialog) }, - ) - .fillMaxSize() - .padding(all = 32.dp), - contentAlignment = Alignment.Center, - ) { - DecoratedBox { - Surface { - Column( - verticalArrangement = Arrangement.spacedBy(space = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally, + + Row( + horizontalArrangement = Arrangement.spacedBy(space = spacings.width), + verticalAlignment = Alignment.Bottom, + ) { + SignButton( + modifier = Modifier + .size(size = 56.dp) + .background( + color = MaterialTheme.lwa.colorScheme.elevated.base1dp, + shape = MaterialTheme.shapes.small, + ), + add = state.add, + onClick = { onSwapSign(state) }, + ) + LwaTextField( + modifier = Modifier + .focusRequester(focusRequester = focusRequester) + .width(192.dp), + field = state.value, + ) + } + state.enableArmor?.let { + Row ( + modifier = Modifier.align(alignment = Alignment.End), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = spacings.width, + alignment = Alignment.End + ), ) { Text( - modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp), style = MaterialTheme.typography.caption, - text = dialog.label, + text = stringResource(Res.string.character_sheet_edit__use_armor), + ) + LwaCheckBox( + field = it, + ) + } + } + Row( + modifier = Modifier.align(alignment = Alignment.End), + horizontalArrangement = Arrangement.spacedBy( + space = spacings.width, + alignment = Alignment.End + ) + ) { + TextButton( + onClick = onDismissRequest, + ) { + Text( + color = MaterialTheme.colors.primary.copy(alpha = .7f), + text = stringResource(Res.string.dialog__cancel_action) + ) + } + TextButton( + enabled = state.enableConfirm.collectAsState().value, + onClick = { onConfirm(state) }, + ) { + Text( + text = stringResource(Res.string.dialog__confirm_action) ) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - BasicTextField( - modifier = Modifier - .focusRequester(focusRequester = focusRequester) - .alignByBaseline() - .width(width = 120.dp), - textStyle = remember { - typography.h5.copy( - color = colors.primary, - textAlign = TextAlign.End, - fontWeight = FontWeight.Bold - ) - }, - cursorBrush = SolidColor(MaterialTheme.colors.primary), - singleLine = true, - keyboardActions = KeyboardActions { onConfirm(dialog) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - value = dialog.value(), - onValueChange = dialog.onValueChange, - ) - Text( - modifier = Modifier.alignByBaseline(), - text = "/", - ) - Text( - modifier = Modifier.alignByBaseline(), - text = dialog.maxValue, - ) - } - Row( - modifier = Modifier - .padding(bottom = 4.dp) - .padding(horizontal = 16.dp) - .align(alignment = Alignment.End), - horizontalArrangement = Arrangement.spacedBy( - space = 4.dp, - alignment = Alignment.End - ) - ) { - TextButton( - onClick = onDismissRequest, - ) { - Text( - color = MaterialTheme.colors.primary.copy(alpha = .7f), - text = stringResource(Res.string.dialog__cancel_action) - ) - } - TextButton( - onClick = { onConfirm(dialog) }, - ) { - Text( - text = stringResource(Res.string.dialog__confirm_action) - ) - } - } } } } } +} + + +@Composable +private fun CharacterSheetCharacteristicDialogKeyHandler( + onSwap: () -> Unit, +) { + KeyHandler { + if (it.type == KeyEventType.KeyDown) { + when (it.key) { + Key.AltLeft, Key.AltRight -> { + onSwap() + true + } + + else -> false + } + } else { + false + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt index 34746a4..c995855 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt @@ -1,15 +1,22 @@ package com.pixelized.desktop.lwa.ui.composable.character.characteristic -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio.Characteristic +import com.pixelized.desktop.lwa.ui.composable.checkbox.createLwaCheckBox +import com.pixelized.desktop.lwa.ui.composable.checkbox.createLwaCheckBoxFlow +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character__inventory__add_to_purse__title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point import org.jetbrains.compose.resources.getString @@ -19,9 +26,13 @@ class CharacterSheetCharacteristicDialogFactory( private val alterationRepository: AlterationRepository, private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, ) { + private val decimalChecker = Regex("""^\s*\d*\s*${'$'}""") + suspend fun convertToDialogUio( + scope: CoroutineScope, characterSheetId: String?, characteristic: Characteristic, + signFlow: StateFlow, ): CharacterSheetCharacteristicDialogUio? { if (characterSheetId == null) return null @@ -32,10 +43,10 @@ class CharacterSheetCharacteristicDialogFactory( if (characterSheet == null) return null - val alterations: Map> = alterationRepository.activeFieldAlterations( - characterSheetId = characterSheetId, - ) - + val alterations: Map> = + alterationRepository.activeFieldAlterations( + characterSheetId = characterSheetId, + ) val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( characterSheet = characterSheet, alterations = alterations, @@ -43,36 +54,61 @@ class CharacterSheetCharacteristicDialogFactory( return when (characteristic) { Characteristic.Damage -> { - val value = mutableStateOf( - "${alteredCharacterSheet.maxHp - alteredCharacterSheet.damage}".let { - TextFieldValue(text = it, selection = TextRange(it.length)) - } + val valueFlow = createLwaTextFieldFlow( + label = null, ) + val armor = if (alteredCharacterSheet.armor > 0) { + createLwaCheckBoxFlow( + checked = true, + ) + } else { + null + } CharacterSheetCharacteristicDialogUio( characterSheetId = characterSheetId, characteristic = characteristic, - label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point), - value = { value.value }, - onValueChange = { value.value = it }, - maxValue = "${alteredCharacterSheet.maxHp}", + label = getString(Res.string.character_sheet_edit__sub_characteristics__hit_point).let { + "$it : ${alteredCharacterSheet.maxHp - alteredCharacterSheet.damage}/${alteredCharacterSheet.maxHp}" + }, + add = signFlow, + value = valueFlow.createLwaTextField { + valueFlow.errorFlow.value = check(it) + valueFlow.valueFlow.value = it + }, + enableArmor = armor?.createLwaCheckBox(), + enableConfirm = valueFlow.errorFlow.map { it.not() }.stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = valueFlow.errorFlow.value.not(), + ), ) } Characteristic.Fatigue -> { - val value = mutableStateOf( - "${alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue}".let { - TextFieldValue(text = it, selection = TextRange(it.length)) - } + val valueFlow = createLwaTextFieldFlow( + label = null, ) CharacterSheetCharacteristicDialogUio( characterSheetId = characterSheetId, characteristic = characteristic, - label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point), - value = { value.value }, - onValueChange = { value.value = it }, - maxValue = "${alteredCharacterSheet.maxPp}", + label = getString(Res.string.character_sheet_edit__sub_characteristics__power_point).let { + "$it : ${alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue}/${alteredCharacterSheet.maxPp}" + }, + add = signFlow, + value = valueFlow.createLwaTextField { + valueFlow.errorFlow.value = check(it) + valueFlow.valueFlow.value = it + }, + enableArmor = null, + enableConfirm = valueFlow.errorFlow.map { it.not() }.stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = valueFlow.errorFlow.value.not(), + ), ) } } } + + private fun check(value: String): Boolean = !decimalChecker.matches(value) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogViewModel.kt index 9ea4c93..7e104d4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogViewModel.kt @@ -3,12 +3,15 @@ package com.pixelized.desktop.lwa.ui.composable.character.characteristic import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio.Characteristic import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update class CharacterSheetCharacteristicDialogViewModel( private val characterSheetRepository: CharacterSheetRepository, @@ -18,6 +21,8 @@ class CharacterSheetCharacteristicDialogViewModel( private val network: NetworkRepository, ) : ViewModel() { + private val signFlow = MutableStateFlow(false) + private val _statChangeDialog = mutableStateOf(null) val statChangeDialog: State get() = _statChangeDialog @@ -25,19 +30,27 @@ class CharacterSheetCharacteristicDialogViewModel( _statChangeDialog.value = null } + fun swapCharacteristicSign() { + signFlow.update { it.not() } + } + suspend fun showSubCharacteristicDialog( characterSheetId: String?, characteristic: Characteristic, ) { + signFlow.update { false } _statChangeDialog.value = factory.convertToDialogUio( + scope = viewModelScope, characterSheetId = characterSheetId, characteristic = characteristic, + signFlow = signFlow, ) } suspend fun changeSubCharacteristic( characterSheetId: String?, characteristic: Characteristic, + useArmor: Boolean, value: Int, ) { if (characterSheetId == null) return @@ -49,29 +62,37 @@ class CharacterSheetCharacteristicDialogViewModel( if (characterSheet == null) return - val alterations = alterationRepository.activeFieldAlterations( - characterSheetId = characterSheetId, - ) - - // we need the maximum HP / Power that the character sheet have. - val alteredSheet = alteredCharacterSheetFactory.sheet( - characterSheet = characterSheet, - alterations = alterations, - ) + val sign = if (signFlow.value) 1 else -1 val message = when (characteristic) { - Characteristic.Damage -> CharacterSheetEvent.UpdateDamage( - timestamp = System.currentTimeMillis(), - characterSheetId = characterSheetId, - oldValue = alteredSheet.damage, - damage = alteredSheet.maxHp - value, - ) + Characteristic.Damage -> { + val armor = if (useArmor && signFlow.value.not()) { + val alterations = alterationRepository.activeFieldAlterations( + characterSheetId = characterSheetId, + ) + val alteredSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = alterations, + ) + alteredSheet.armor + } else { + null + } + + CharacterSheetEvent.UpdateDamage( + timestamp = System.currentTimeMillis(), + characterSheetId = characterSheetId, + oldValue = characterSheet.damage, + damage = characterSheet.damage - value * sign - (armor ?: 0), + armor = armor, + ) + } Characteristic.Fatigue -> CharacterSheetEvent.UpdateFatigue( timestamp = System.currentTimeMillis(), characterSheetId = characterSheetId, - oldValue = alteredSheet.fatigue, - fatigue = alteredSheet.maxPp - value, + oldValue = characterSheet.fatigue, + fatigue = characterSheet.fatigue - value * sign, ) } 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 8a6e708..9c4c360 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 @@ -1,9 +1,6 @@ package com.pixelized.desktop.lwa.ui.composable.character.purse import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith @@ -19,8 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -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 @@ -35,13 +30,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.button.SignButton import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler @@ -53,8 +48,6 @@ import kotlinx.coroutines.flow.StateFlow import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action -import lwacharactersheet.composeapp.generated.resources.ic_remove_24dp -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Stable @@ -94,6 +87,10 @@ fun PurseDialog( val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.requestFocus() } + PurseDialogKeyHandler( + onSwap = { onSwapSign(state) }, + ) + Column( modifier = Modifier.padding(paddingValues = paddings), verticalArrangement = Arrangement.spacedBy(space = spacings.height), @@ -162,10 +159,6 @@ fun PurseDialog( } } } - - PurseDialogKeyHandler( - onSwap = { onSwapSign(state) }, - ) } } @@ -202,46 +195,6 @@ private fun PurseContent( } } -@Composable -private fun SignButton( - modifier: Modifier = Modifier, - interactionSource: MutableInteractionSource? = null, - enabled: Boolean = true, - add: StateFlow, - onClick: () -> Unit, -) { - IconButton( - onClick = onClick, - modifier = modifier, - enabled = enabled, - interactionSource = interactionSource, - ) { - val rotation = animateFloatAsState( - animationSpec = spring(stiffness = Spring.StiffnessMediumLow), - targetValue = when (add.collectAsState().value) { - true -> 90f - else -> 0f - } - ) - Icon( - modifier = Modifier.graphicsLayer { - this.rotationZ = rotation.value * 2f - }, - painter = painterResource(Res.drawable.ic_remove_24dp), - tint = MaterialTheme.lwa.colorScheme.base.primary, - contentDescription = null - ) - Icon( - modifier = Modifier.graphicsLayer { - this.rotationZ = rotation.value * 3f - }, - painter = painterResource(Res.drawable.ic_remove_24dp), - tint = MaterialTheme.lwa.colorScheme.base.primary, - contentDescription = null - ) - } -} - @Composable private fun PurseDialogKeyHandler( onSwap: () -> Unit, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/checkbox/LwaCheckBoxHelper.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/checkbox/LwaCheckBoxHelper.kt new file mode 100644 index 0000000..7caea00 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/checkbox/LwaCheckBoxHelper.kt @@ -0,0 +1,30 @@ +package com.pixelized.desktop.lwa.ui.composable.checkbox + +import androidx.compose.runtime.Stable +import kotlinx.coroutines.flow.MutableStateFlow + +@Stable +data class LwaCheckBoxFlow( + val checkedFlow: MutableStateFlow, +) + +@Stable +fun createLwaCheckBoxFlow( + checked: Boolean, +): LwaCheckBoxFlow { + return LwaCheckBoxFlow( + checkedFlow = MutableStateFlow(checked), + ) +} + +@Stable +fun LwaCheckBoxFlow.createLwaCheckBox( + enable: Boolean = true, + onCheckedChange: (Boolean) -> Unit = { checkedFlow.value = it }, +): LwaCheckBoxUio { + return LwaCheckBoxUio( + enable = enable, + checked = checkedFlow, + onCheckedChange = onCheckedChange, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextFieldHelper.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextFieldHelper.kt index 9bc4fe0..b1e88a8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextFieldHelper.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextFieldHelper.kt @@ -6,13 +6,13 @@ import kotlinx.coroutines.flow.MutableStateFlow @Stable data class LwaTextFieldFlow( val errorFlow: MutableStateFlow, - val valueFlow: MutableStateFlow, val labelFlow: MutableStateFlow, + val valueFlow: MutableStateFlow, ) fun createLwaTextFieldFlow( error: Boolean = false, - label: String, + label: String?, value: String = "", ): LwaTextFieldFlow { return createLwaTextFieldFlow( @@ -24,8 +24,8 @@ fun createLwaTextFieldFlow( fun createLwaTextFieldFlow( errorFlow: MutableStateFlow = MutableStateFlow(false), - valueFlow: MutableStateFlow, labelFlow: MutableStateFlow, + valueFlow: MutableStateFlow, ): LwaTextFieldFlow { return LwaTextFieldFlow( errorFlow = errorFlow, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index 0fafa77..c18e99b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -206,12 +206,16 @@ fun CampaignScreen( characteristicDialogViewModel.changeSubCharacteristic( characterSheetId = dialog.characterSheetId, characteristic = dialog.characteristic, - value = dialog.value().text.toIntOrNull() ?: 0, + useArmor= dialog.enableArmor?.checked?.value == true, + value = dialog.value.valueFlow.value.toIntOrNull() ?: 0, ) characteristicDialogViewModel.hideSubCharacteristicDialog() blurController.hide() } }, + onSwapSign = { + characteristicDialogViewModel.swapCharacteristicSign() + }, onDismissRequest = { characteristicDialogViewModel.hideSubCharacteristicDialog() blurController.hide() diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt index cbb6ece..373c3a4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt @@ -17,6 +17,7 @@ import com.pixelized.shared.lwa.protocol.websocket.RollEvent import com.pixelized.shared.lwa.protocol.websocket.SocketMessage import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_down +import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_down_armor import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_up import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__pp_down import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__pp_up @@ -94,16 +95,19 @@ class TextMessageFactory( ?: return null // We are talking about damage / consumption there so old value have to be put first. val value = message.oldValue - message.damage + val armor = message.armor CharacteristicTextMessageUio( id = "${message.timestamp}-${message.characterSheetId}-Damage", timestamp = formatTime.format(message.timestamp), label = when { - value < 0 -> Res.string.chat__characteristic_change__hp_down - else -> Res.string.chat__characteristic_change__hp_up + value >= 0 -> Res.string.chat__characteristic_change__hp_up + armor != null -> Res.string.chat__characteristic_change__hp_down_armor + else -> Res.string.chat__characteristic_change__hp_down }, character = sheet.name, value = abs(value), + armor = message.armor, ) } @@ -127,6 +131,7 @@ class TextMessageFactory( }, character = sheet.name, value = abs(value), + armor = 0, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt index 9e31927..40568f8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt @@ -21,6 +21,7 @@ data class CharacteristicTextMessageUio( val label: StringResource, val character: String, val value: Int, + val armor: Int?, ) : TextMessage @Composable @@ -52,7 +53,7 @@ fun CharacteristicTextMessage( style = MaterialTheme.lwa.typography.chat.text, overflow = TextOverflow.Ellipsis, maxLines = 1, - text = stringResource(message.label, message.character, message.value), + text = stringResource(message.label, message.character, message.value, message.armor ?: 0), ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt index cb1bdb9..79899c8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt @@ -126,12 +126,16 @@ fun GMCharacterPage( characteristicDialogViewModel.changeSubCharacteristic( characterSheetId = dialog.characterSheetId, characteristic = dialog.characteristic, - value = dialog.value().text.toIntOrNull() ?: 0, + useArmor= dialog.enableArmor?.checked?.value == true, + value = dialog.value.valueFlow.value.toIntOrNull() ?: 0, ) characteristicDialogViewModel.hideSubCharacteristicDialog() blurController.hide() } }, + onSwapSign = { + characteristicDialogViewModel.swapCharacteristicSign() + }, onDismissRequest = { characteristicDialogViewModel.hideSubCharacteristicDialog() blurController.hide() diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character_Damage.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character_Damage.kt index 5c8094d..aa60fd6 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character_Damage.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character_Damage.kt @@ -18,6 +18,10 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit { name = "damage", code = APIResponse.ErrorCode.Damage, ) + val armor: Int = call.queryParameters.param( + name = "armor", + code = APIResponse.ErrorCode.Armor, + ) // fetch the character sheet val characterSheet = characterService.character(characterSheetId) ?: error("CharacterSheet with id:$characterSheetId not found.") @@ -36,6 +40,7 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit { characterSheetId = characterSheetId, oldValue = characterSheet.damage, damage = damage, + armor = armor, ) ) } catch (exception: Exception) { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt index b058d0d..b58c0e9 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt @@ -22,6 +22,7 @@ data class APIResponse( Active, Equip, Damage, + Armor, Fatigue, Diminished, Count, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CharacterSheetEvent.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CharacterSheetEvent.kt index 408d131..37c24d2 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CharacterSheetEvent.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CharacterSheetEvent.kt @@ -9,6 +9,7 @@ sealed interface CharacterSheetEvent : SocketMessage, CharacterSheetIdMessage { data class UpdateDamage( override val characterSheetId: String, override val timestamp: Long, + val armor: Int?, val oldValue: Int, val damage: Int, ) : CharacterSheetEvent From 48a461396a6b3112b53fc478809d31399d6619b7 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sun, 15 Jun 2025 14:52:55 +0200 Subject: [PATCH 09/10] Bump version to 1.5.1 --- composeApp/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index ab0b8f3..0bd91d4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -9,7 +9,7 @@ plugins { alias(libs.plugins.buildkonfig) } -fun getVersion() = "1.5.0" +fun getLwaVersion() = "1.5.1" kotlin { jvm("desktop") @@ -67,7 +67,7 @@ buildkonfig { packageName = "com.pixelized.desktop.lwa" defaultConfigs { - buildConfigField(STRING, "version", getVersion()) + buildConfigField(STRING, "version", getLwaVersion()) } } @@ -80,7 +80,7 @@ compose.desktop { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Table de Lwa" - packageVersion = getVersion() + packageVersion = getLwaVersion() description = "Application de support au jeux de rôle dans l'univers de Lwa." copyright = "© 2020 Pixelized. All rights reserved." vendor = "Pixelized" From 9be8f2b20958996af760b9004462980ef464b556 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Mon, 16 Jun 2025 23:44:58 +0200 Subject: [PATCH 10/10] Fix regression when adding an non stackable item. --- .../composable/character/item/ItemDetailDialogViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index fb5c403..974468d 100644 --- 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 @@ -86,9 +86,9 @@ class ItemDetailDialogViewModel( ): Boolean { try { if (dialog.countable?.errorFlow?.value == true) return false - val quantity = dialog.countable?.valueFlow?.value ?: return false - val count = factory.parse(quantity = quantity) - ?: quantity.toFloatOrNull() + val quantity = dialog.countable?.valueFlow?.value + val count = factory.parse(quantity = quantity ?: "") + ?: quantity?.toFloatOrNull() ?: 1f // create the inventory item on the server, get the newly create id from that. inventoryRepository.createInventoryItem(