From 1fe75062b771a43275f330eedd9d663a2a829a14 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Thu, 27 Feb 2025 15:36:07 +0100 Subject: [PATCH] Add action to the character sheet detail panel. --- .../roll_history/RollHistoryRepository.kt | 20 -- .../lwa/ui/composable/circle/MasteryShape.kt | 69 +++++++ .../campaign/player/detail/CharacterDetail.kt | 10 + .../player/detail/CharacterDetailFactory.kt | 24 ++- .../detail/sheet/CharacterDetailSheet.kt | 99 +++++++-- .../sheet/CharacterDetailSheetAction.kt | 61 ++++++ .../detail/sheet/CharacterDetailSheetSkill.kt | 16 +- .../lwa/ui/screen/roll/RollViewModel.kt | 10 +- .../desktop/lwa/utils/extention/JsonExt.kt | 5 +- .../extention/ModifierEx+DashedBorder.kt | 195 ++++++++++++++++++ server/build.gradle.kts | 1 + .../pixelized/server/lwa/extention/JsonExt.kt | 6 +- .../com/pixelized/server/lwa/server/Server.kt | 3 +- .../protocol/websocket/payload/RollMessage.kt | 6 +- 14 files changed, 469 insertions(+), 56 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+DashedBorder.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt index 4b20e77..633f3b5 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/roll_history/RollHistoryRepository.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn -import kotlinx.serialization.json.Json class RollHistoryRepository( private val network: NetworkRepository, @@ -21,23 +20,4 @@ class RollHistoryRepository( scope = scope, started = SharingStarted.Eagerly, ) - - suspend fun share( - characterId: String, - skillLabel: String, - rollDifficulty: String?, - rollValue: Int, - resultLabel: String?, - rollSuccessLimit: Int?, - ) { - val content = RollMessage( - characterId = characterId, - skillLabel = skillLabel, - rollDifficulty = rollDifficulty, - rollValue = rollValue, - resultLabel = resultLabel, - rollSuccessLimit = rollSuccessLimit, - ) - network.share(payload = content) - } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt new file mode 100644 index 0000000..28152aa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt @@ -0,0 +1,69 @@ +package com.pixelized.desktop.lwa.ui.composable.circle + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.utils.extention.dashedBorder + +@Composable +fun MasteryShape( + modifier: Modifier = Modifier, + shape: Shape = CircleShape, + color: Color = MaterialTheme.lwa.colorScheme.base.onSurface, + borderWidth: Dp = 1.dp, + size: Dp = 12.dp, + multiplier: Int, +) { + when (multiplier) { + 0 -> Box( + modifier = modifier + .size(size = size) + .dashedBorder( + width = borderWidth, + color = color, + shape = shape, + on = 3.dp, + off = 2.dp + ) + ) + + 1 -> Box( + modifier = modifier + .size(size = size) + .background( + color = color, + shape = shape, + ) + ) + + else -> Box( + modifier = modifier + .size(size = size) + .border( + width = borderWidth, + color = color, + shape = shape + ) + .padding(all = 2.dp) + ) { + MasteryShape( + shape = shape, + color = color, + borderWidth = borderWidth, + size = size - borderWidth * 2, + multiplier = multiplier - 1, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt index b70060f..8afbc4c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt @@ -30,6 +30,7 @@ import com.pixelized.desktop.lwa.ui.composable.character.characteristic.Characte import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeader import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheet +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio @@ -108,6 +109,11 @@ fun CharacterDetailPanel( ) } }, + onAction = { + rollViewModel.prepareRoll(roll = it.roll) + rollViewModel.showOverlay() + blurController.show() + } ) } @@ -122,6 +128,7 @@ fun CharacterDetailAnimatedPanel( onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit, onSkill: (CharacterDetailSheetSkillUio) -> Unit, onUseSkill: (CharacterDetailSheetSkillUio) -> Unit, + onAction: (CharacterDetailSheetActionUio) -> Unit, ) { Box( modifier = modifier, @@ -159,6 +166,7 @@ fun CharacterDetailAnimatedPanel( onCharacteristic = onCharacteristic, onSkill = onSkill, onUseSkill = onUseSkill, + onAction = onAction, ) } } @@ -179,6 +187,7 @@ fun CharacterDetailContent( onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit, onSkill: (CharacterDetailSheetSkillUio) -> Unit, onUseSkill: (CharacterDetailSheetSkillUio) -> Unit, + onAction: (CharacterDetailSheetActionUio) -> Unit, ) { Surface( modifier = modifier.fillMaxSize(), @@ -206,6 +215,7 @@ fun CharacterDetailContent( onCharacteristic = onCharacteristic, onSkill = onSkill, onUseSkill = onUseSkill, + onAction = onAction, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt index a6be22f..f952a41 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt @@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio @@ -192,6 +193,7 @@ class CharacterDetailFactory( label = skill.label, value = "$value", used = skill.used, + occupation = skill.occupation, tooltips = skill.description?.let { TooltipUio( title = skill.label, @@ -217,6 +219,7 @@ class CharacterDetailFactory( label = skill.label, value = "$value", used = skill.used, + occupation = skill.occupation, tooltips = skill.description?.let { TooltipUio( title = skill.label, @@ -231,7 +234,7 @@ class CharacterDetailFactory( ), ) }.sortedWith(compareBy(Collator.getInstance()) { it.label }), - magicSkill = characterSheet.magicSkills.map { skill -> + magicSkills = characterSheet.magicSkills.map { skill -> val value = expressionUseCase.computeSkillValue( sheet = characterSheet, skill = skill, @@ -242,6 +245,7 @@ class CharacterDetailFactory( label = skill.label, value = "$value", used = skill.used, + occupation = skill.occupation, tooltips = skill.description?.let { TooltipUio( title = skill.label, @@ -256,6 +260,24 @@ class CharacterDetailFactory( ), ) }.sortedWith(compareBy(Collator.getInstance()) { it.label }), + actions = characterSheet.actions.map { action -> + CharacterDetailSheetActionUio( + actionId = action.id, + label = action.label, + tooltips = action.description?.let { + TooltipUio( + title = action.label, + description = it, + ) + }, + roll = RollActionUio( + characterSheetId = characterInstanceId.characterSheetId, + label = action.label, + rollAction = action.roll, + rollSuccessValue = null, + ) + ) + } ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt index 62f932a..a51d5c7 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt @@ -1,25 +1,36 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +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 import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.shared.lwa.model.campaign.Campaign +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__special_title +import org.jetbrains.compose.resources.stringResource @Stable data class CharacterDetailSheetUio( val characterInstanceId: Campaign.CharacterInstance.Id, val characteristics: List, val commonSkills: List, - val specialSkill: List, - val magicSkill: List, + val specialSkills: List, + val magicSkills: List, + val actions: List, ) @Composable @@ -29,6 +40,7 @@ fun CharacterDetailSheet( onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit, onSkill: (CharacterDetailSheetSkillUio) -> Unit, onUseSkill: (CharacterDetailSheetSkillUio) -> Unit, + onAction: (CharacterDetailSheetActionUio) -> Unit, ) { Row( modifier = modifier, @@ -39,7 +51,7 @@ fun CharacterDetailSheet( ) { sheet.value?.characteristics?.forEach { CharacterDetailSheetCharacteristic( - modifier = Modifier.size(width = 80.dp, height = 120.dp), + modifier = Modifier.size(width = 76.dp, height = 110.dp), characteristic = it, onClick = { onCharacteristic(it) }, ) @@ -47,12 +59,20 @@ fun CharacterDetailSheet( } Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(space = 8.dp) + verticalArrangement = Arrangement.spacedBy(space = 16.dp) ) { DecoratedBox( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), ) { Column { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, top = 4.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__skills__common_title), + ) sheet.value?.commonSkills?.forEach { skill -> CharacterDetailSheetSkill( modifier = Modifier.fillMaxWidth(), @@ -63,30 +83,67 @@ fun CharacterDetailSheet( } } } - DecoratedBox( - modifier = Modifier.fillMaxWidth(), + AnimatedVisibility( + visible = sheet.value?.specialSkills?.isNotEmpty() ?: false, ) { - Column { - sheet.value?.specialSkill?.forEach { skill -> - CharacterDetailSheetSkill( - modifier = Modifier.fillMaxWidth(), - skill = skill, - onSkill = onSkill, - onUse = onUseSkill, + DecoratedBox( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, top = 4.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__skills__special_title), ) + sheet.value?.specialSkills?.forEach { skill -> + CharacterDetailSheetSkill( + modifier = Modifier.fillMaxWidth(), + skill = skill, + onSkill = onSkill, + onUse = onUseSkill, + ) + } } } } - DecoratedBox( - modifier = Modifier.fillMaxWidth(), + AnimatedVisibility( + visible = sheet.value?.magicSkills?.isNotEmpty() ?: false, + ) { + DecoratedBox( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, top = 4.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(Res.string.character_sheet__skills__magic_title), + ) + sheet.value?.magicSkills?.forEach { skill -> + CharacterDetailSheetSkill( + modifier = Modifier.fillMaxWidth(), + skill = skill, + onSkill = onSkill, + onUse = onUseSkill, + ) + } + } + } + } + AnimatedVisibility( + visible = sheet.value?.actions?.isNotEmpty() ?: false, ) { Column { - sheet.value?.magicSkill?.forEach { skill -> - CharacterDetailSheetSkill( + sheet.value?.actions?.forEach { action -> + CharacterDetailSheetAction( modifier = Modifier.fillMaxWidth(), - skill = skill, - onSkill = onSkill, - onUse = onUseSkill, + action = action, + onClick = onAction, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt new file mode 100644 index 0000000..2d50df7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt @@ -0,0 +1,61 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet + +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.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio +import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp +import org.jetbrains.compose.resources.painterResource + +@Stable +data class CharacterDetailSheetActionUio( + val actionId: String, + val label: String, + val tooltips: TooltipUio?, + val roll: RollActionUio, +) + +@Composable +fun CharacterDetailSheetAction( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(start = 28.dp, end = 9.dp, top = 6.dp, bottom = 6.dp), + action: CharacterDetailSheetActionUio, + onClick: (CharacterDetailSheetActionUio) -> Unit, +) { + Row( + modifier = Modifier + .clickable(onClick = { onClick(action) }) + .padding(paddingValues = paddingValues) + .then(other = modifier), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = action.label, + ) + Icon( + modifier = Modifier.size(size = 24.dp), + painter = painterResource(Res.drawable.ic_d20_24dp), + tint = MaterialTheme.colors.primary, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt index 4da5484..3b8d66f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt @@ -5,8 +5,11 @@ 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.Spacer +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.MaterialTheme @@ -18,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.circle.MasteryShape import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio @@ -28,6 +32,7 @@ data class CharacterDetailSheetSkillUio( val label: String, val value: String, val used: Boolean, + val occupation: Boolean, val tooltips: TooltipUio?, val roll: RollActionUio, ) @@ -36,7 +41,12 @@ data class CharacterDetailSheetSkillUio( @Composable fun CharacterDetailSheetSkill( modifier: Modifier = Modifier, - paddingValues: PaddingValues = PaddingValues(start = 8.dp), + paddingValues: PaddingValues = PaddingValues( + start = 8.dp, + end = 4.dp, + top = 1.dp, + bottom = 1.dp + ), skill: CharacterDetailSheetSkillUio, onSkill: (CharacterDetailSheetSkillUio) -> Unit, onUse: (CharacterDetailSheetSkillUio) -> Unit, @@ -52,6 +62,10 @@ fun CharacterDetailSheetSkill( horizontalArrangement = Arrangement.spacedBy(space = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { + MasteryShape( + modifier = Modifier.padding(top = 4.dp, end = 2.dp), + multiplier = if (skill.occupation) 1 else 0, + ) Text( modifier = Modifier.weight(1f), style = MaterialTheme.typography.body1, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt index d6557da..da0bd36 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt @@ -7,10 +7,11 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository -import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository +import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet +import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage import com.pixelized.shared.lwa.usecase.ExpressionUseCase import com.pixelized.shared.lwa.usecase.SkillStepUseCase import kotlinx.coroutines.Job @@ -32,9 +33,9 @@ import org.jetbrains.compose.resources.getString class RollViewModel( private val characterSheetRepository: CharacterSheetRepository, - private val rollHistoryRepository: RollHistoryRepository, private val skillComputation: ExpressionUseCase, private val skillStepUseCase: SkillStepUseCase, + private val networkRepository: NetworkRepository, ) : ViewModel() { private lateinit var sheet: CharacterSheet @@ -205,7 +206,7 @@ class RollViewModel( value = roll, ) launch { - rollHistoryRepository.share( + val payload = RollMessage( characterId = sheet.id, skillLabel = _rollTitle.value.label, rollDifficulty = when (_rollDifficulty.value?.difficulty) { @@ -219,6 +220,9 @@ class RollViewModel( rollSuccessLimit = rollStep?.success?.last, resultLabel = success, ) + networkRepository.share( + payload = payload, + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt index 55fd8ee..d746fc2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt @@ -4,7 +4,6 @@ import com.pixelized.shared.lwa.protocol.websocket.Message import io.ktor.websocket.Frame import io.ktor.websocket.readText import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToJsonElement fun Json.decodeFromFrame(frame: Frame.Text): Message { val json = frame.readText() @@ -12,6 +11,6 @@ fun Json.decodeFromFrame(frame: Frame.Text): Message { } fun Json.encodeToFrame(message: Message): Frame { - val json = encodeToJsonElement(message) - return Frame.Text(text = json.toString()) + val json = encodeToString(message) + return Frame.Text(text = json) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+DashedBorder.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+DashedBorder.kt new file mode 100644 index 0000000..54dd759 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/ModifierEx+DashedBorder.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pixelized.desktop.lwa.utils.extention + +import androidx.compose.foundation.BorderStroke +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.isSimple +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.unit.Dp + + +/** + * Modify element to add border with appearance specified with a [border] and a [shape], pad the + * content by the [BorderStroke.width] and clip it. + * + * @sample androidx.compose.foundation.samples.BorderSample() + * + * @param border [BorderStroke] class that specifies border appearance, such as size and color + * @param shape shape of the border + */ +fun Modifier.dashedBorder(border: BorderStroke, shape: Shape = RectangleShape, on: Dp, off: Dp) = + dashedBorder(width = border.width, brush = border.brush, shape = shape, on, off) + +/** + * Returns a [Modifier] that adds border with appearance specified with [width], [color] and a + * [shape], pads the content by the [width] and clips it. + * + * @sample androidx.compose.foundation.samples.BorderSampleWithDataClass() + * + * @param width width of the border. Use [Dp.Hairline] for a hairline border. + * @param color color to paint the border with + * @param shape shape of the border + * @param on the size of the solid part of the dashes + * @param off the size of the space between dashes + */ +fun Modifier.dashedBorder(width: Dp, color: Color, shape: Shape = RectangleShape, on: Dp, off: Dp) = + dashedBorder(width, SolidColor(color), shape, on, off) + +/** + * Returns a [Modifier] that adds border with appearance specified with [width], [brush] and a + * [shape], pads the content by the [width] and clips it. + * + * @sample androidx.compose.foundation.samples.BorderSampleWithBrush() + * + * @param width width of the border. Use [Dp.Hairline] for a hairline border. + * @param brush brush to paint the border with + * @param shape shape of the border + */ +fun Modifier.dashedBorder(width: Dp, brush: Brush, shape: Shape, on: Dp, off: Dp): Modifier = + this.then( + Modifier.drawWithCache { + val outline: Outline = shape.createOutline(size, layoutDirection, this) + val borderSize = if (width == Dp.Hairline) 1f else width.toPx() + + var insetOutline: Outline? = null // outline used for roundrect/generic shapes + var stroke: Stroke? = null // stroke to draw border for all outline types + var pathClip: Path? = null // path to clip roundrect/generic shapes + var inset = 0f // inset to translate before drawing the inset outline + // path to draw generic shapes or roundrects with different corner radii + var insetPath: Path? = null + if (borderSize > 0 && size.minDimension > 0f) { + if (outline is Outline.Rectangle) { + stroke = Stroke( + borderSize, pathEffect = PathEffect.dashPathEffect( + floatArrayOf(on.toPx(), off.toPx()) + ) + ) + } else { + // Multiplier to apply to the border size to get a stroke width that is + // large enough to cover the corners while not being too large to overly + // square off the internal shape. The resultant shape will be + // clipped to the desired shape. Any value lower will show artifacts in + // the corners of shapes. A value too large will always square off + // the internal shape corners. For example, for a rounded rect border + // a large multiplier will always have squared off edges within the + // inner section of the stroke, however, having a smaller multiplier + // will still keep the rounded effect for the inner section of the + // border + val strokeWidth = 1.2f * borderSize + inset = borderSize - strokeWidth / 2 + val insetSize = Size( + size.width - inset * 2, + size.height - inset * 2 + ) + insetOutline = shape.createOutline(insetSize, layoutDirection, this) + stroke = Stroke( + strokeWidth, pathEffect = PathEffect.dashPathEffect( + floatArrayOf(on.toPx(), off.toPx()) + ) + ) + pathClip = when (outline) { + is Outline.Rounded -> { + Path().apply { addRoundRect(outline.roundRect) } + } + + is Outline.Generic -> { + outline.path + } + // should not get here because we check for Outline.Rectangle above + else -> { + null + } + } + + insetPath = when { + // Rounded rect with non equal corner radii needs a path to be pre-translated + insetOutline is Outline.Rounded && !insetOutline.roundRect.isSimple -> { + Path().apply { + addRoundRect(insetOutline.roundRect) + translate(Offset(inset, inset)) + } + } + // Generic paths must be created and pre-translated + insetOutline is Outline.Generic -> { + + Path().apply { + addPath(insetOutline.path, Offset(inset, inset)) + } + } + // Drawing a round rect with equal corner radii without usage of a path + else -> { + null + } + } + } + } + + onDrawWithContent { + drawContent() + // Only draw the border if a have a valid stroke parameter. If we have + // an invalid border size we will just draw the content + if (stroke != null) { + if (insetOutline != null && pathClip != null) { + val isSimpleRoundRect = + insetOutline is Outline.Rounded && insetOutline.roundRect.isSimple + withTransform({ + clipPath(pathClip) + // we are drawing the round rect not as a path so we must + // translate ourselves othe + if (isSimpleRoundRect) { + translate(inset, inset) + } + }) { + if (isSimpleRoundRect) { + // If we don't have an insetPath then we are drawing + // a simple round rect with the corner radii all identical + val rrect = (insetOutline as Outline.Rounded).roundRect + drawRoundRect( + brush = brush, + topLeft = Offset(rrect.left, rrect.top), + size = Size(rrect.width, rrect.height), + cornerRadius = rrect.topLeftCornerRadius, + style = stroke + ) + } else if (insetPath != null) { + drawPath(insetPath, brush, style = stroke) + } + } + } else { + // Rectangular border fast path + val strokeWidth = stroke.width + val halfStrokeWidth = strokeWidth / 2 + drawRect( + brush = brush, + topLeft = Offset(halfStrokeWidth, halfStrokeWidth), + size = Size( + size.width - strokeWidth, + size.height - strokeWidth + ), + style = stroke + ) + } + } + } + } + ) \ No newline at end of file diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 0c3d4d3..9f20c68 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.shared) implementation(libs.logback) implementation(libs.koin.ktor) + implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) implementation(libs.ktor.server.websockets) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt index 8c2f2c2..35f8daa 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt @@ -4,7 +4,7 @@ import com.pixelized.shared.lwa.protocol.websocket.Message import io.ktor.websocket.Frame import io.ktor.websocket.readText import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToJsonElement + fun Json.decodeFromFrame(frame: Frame.Text): Message { val json = frame.readText() @@ -12,6 +12,6 @@ fun Json.decodeFromFrame(frame: Frame.Text): Message { } fun Json.encodeToFrame(message: Message): Frame { - val json = encodeToJsonElement(message) - return Frame.Text(text = json.toString()) + val json = encodeToString(message) + return Frame.Text(text = json) } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt index fa34bd9..a894923 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -81,7 +81,8 @@ class LocalServer { val job = launch { // send local message to the clients engine.webSocket.collect { message -> - send(json.encodeToFrame(message)) + val frame = json.encodeToFrame(message) + send(frame) } } runCatching { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt index d88a71e..0b56471 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt @@ -6,8 +6,8 @@ import kotlinx.serialization.Serializable data class RollMessage( val characterId: String, val skillLabel: String, - val resultLabel: String?, - val rollDifficulty: String?, + val resultLabel: String? = null, + val rollDifficulty: String? = null, val rollValue: Int, - val rollSuccessLimit: Int?, + val rollSuccessLimit: Int? = null, ) : MessagePayload \ No newline at end of file