Compare commits
10 commits
292374d547
...
9be8f2b209
| Author | SHA1 | Date | |
|---|---|---|---|
| 9be8f2b209 | |||
| 48a461396a | |||
| 894d8db493 | |||
| 741bb7cf25 | |||
| 51a477bb58 | |||
| 17b7b06ec8 | |||
| 50697ceb93 | |||
|
|
a84c170396 | ||
|
|
5632ec7c85 | ||
|
|
2c4519af3b |
46 changed files with 1108 additions and 821 deletions
|
|
@ -9,7 +9,7 @@ plugins {
|
|||
alias(libs.plugins.buildkonfig)
|
||||
}
|
||||
|
||||
fun getVersion() = "1.4.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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z" />
|
||||
</vector>
|
||||
|
|
@ -84,6 +84,7 @@
|
|||
<string name="character_sheet_edit__sub_characteristics__max_hit_point">Points de vie maximum</string>
|
||||
<string name="character_sheet_edit__sub_characteristics__hit_point">Points de vie</string>
|
||||
<string name="character_sheet_edit__sub_characteristics__max_power_point">Points de pouvoir maximum</string>
|
||||
<string name="character_sheet_edit__use_armor">Utiliser l'armure</string>
|
||||
<string name="character_sheet_edit__sub_characteristics__power_point">Points de pouvoir</string>
|
||||
<string name="character_sheet_edit__sub_characteristics__damage_bonus">Bonus aux dégats</string>
|
||||
<string name="character_sheet_edit__sub_characteristics__armor">Armure</string>
|
||||
|
|
@ -253,6 +254,7 @@
|
|||
|
||||
<string name="chat__diminished_change">%1$s passe à %2$d d'état diminuée</string>
|
||||
<string name="chat__characteristic_change__hp_down">%1$s subit %2$d point(s) de dommage</string>
|
||||
<string name="chat__characteristic_change__hp_down_armor">%1$s subit %2$d point(s) de dommage (armure : %3$d)</string>
|
||||
<string name="chat__characteristic_change__hp_up">%1$s récupère %2$d point(s) de vie</string>
|
||||
<string name="chat__characteristic_change__pp_down">%1$s utilise %2$d point(s) de pouvoir</string>
|
||||
<string name="chat__characteristic_change__pp_up">%1$s récupère %2$d point(s) de pouvoir</string>
|
||||
|
|
@ -284,6 +286,9 @@
|
|||
<string name="level_up__character_level_description">Passage du niveau %1$d ▸ %2$d</string>
|
||||
<string name="level_up__skill_level">niv : %1$d -</string>
|
||||
|
||||
<string name="portrait_overlay__external_link">Lien vers l'image</string>
|
||||
<string name="portrait_overlay__close">Fermer</string>
|
||||
|
||||
<string name="game_master__title">Admin</string>
|
||||
<string name="game_master__action">GameMaster</string>
|
||||
<string name="game_master__action__save">Sauvegarder</string>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Boolean>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean>,
|
||||
val value: LwaTextFieldUio,
|
||||
val enableArmor: LwaCheckBoxUio?,
|
||||
val enableConfirm: StateFlow<Boolean>,
|
||||
) {
|
||||
@Stable
|
||||
enum class Characteristic {
|
||||
|
|
@ -55,125 +59,131 @@ data class CharacterSheetCharacteristicDialogUio(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CharacterSheetCharacteristicDialog(
|
||||
dialog: State<CharacterSheetCharacteristicDialogUio?>,
|
||||
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<CharacterSheetCharacteristicDialogUio?>,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean>,
|
||||
): CharacterSheetCharacteristicDialogUio? {
|
||||
|
||||
if (characterSheetId == null) return null
|
||||
|
|
@ -32,10 +43,10 @@ class CharacterSheetCharacteristicDialogFactory(
|
|||
|
||||
if (characterSheet == null) return null
|
||||
|
||||
val alterations: Map<String, List<FieldAlteration>> = alterationRepository.activeFieldAlterations(
|
||||
characterSheetId = characterSheetId,
|
||||
)
|
||||
|
||||
val alterations: Map<String, List<FieldAlteration>> =
|
||||
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)
|
||||
}
|
||||
|
|
@ -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<CharacterSheetCharacteristicDialogUio?>(null)
|
||||
val statChangeDialog: State<CharacterSheetCharacteristicDialogUio?> 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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<String, List<FieldAlteration>> = 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)}" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Boolean>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<Boolean>,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
|
|
@ -6,13 +6,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
@Stable
|
||||
data class LwaTextFieldFlow(
|
||||
val errorFlow: MutableStateFlow<Boolean>,
|
||||
val valueFlow: MutableStateFlow<String>,
|
||||
val labelFlow: MutableStateFlow<String?>,
|
||||
val valueFlow: MutableStateFlow<String>,
|
||||
)
|
||||
|
||||
fun createLwaTextFieldFlow(
|
||||
error: Boolean = false,
|
||||
label: String,
|
||||
label: String?,
|
||||
value: String = "",
|
||||
): LwaTextFieldFlow {
|
||||
return createLwaTextFieldFlow(
|
||||
|
|
@ -24,8 +24,8 @@ fun createLwaTextFieldFlow(
|
|||
|
||||
fun createLwaTextFieldFlow(
|
||||
errorFlow: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
valueFlow: MutableStateFlow<String>,
|
||||
labelFlow: MutableStateFlow<String?>,
|
||||
valueFlow: MutableStateFlow<String>,
|
||||
): LwaTextFieldFlow {
|
||||
return LwaTextFieldFlow(
|
||||
errorFlow = errorFlow,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Boolean>,
|
||||
val showFilter: StateFlow<Boolean>,
|
||||
val filter: LwaTextFieldUio,
|
||||
val purse: PurseUio,
|
||||
val items: List<InventoryItemUio>,
|
||||
|
|
@ -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<Boolean>)?.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(
|
||||
|
|
|
|||
|
|
@ -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<Boolean>,
|
||||
initialValue: () -> CharacterDetailInventoryUio?,
|
||||
): StateFlow<CharacterDetailInventoryUio?> {
|
||||
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<Boolean>,
|
||||
filter: LwaTextFieldUio,
|
||||
addItemAction: StateFlow<Boolean>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CharacterRibbonRollAnimation, List<Float>> {
|
||||
override fun restore(value: List<Float>): CharacterRibbonRollAnimation {
|
||||
return CharacterRibbonRollAnimation(
|
||||
animatedAlpha = Animatable(value[0]),
|
||||
animatedRotation = Animatable(value[1]),
|
||||
animatedScale = Animatable(value[2]),
|
||||
)
|
||||
}
|
||||
|
||||
override fun SaverScope.save(value: CharacterRibbonRollAnimation): List<Float> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -38,7 +39,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 +73,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(
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
CoroutineScope(Job())
|
||||
}
|
||||
}
|
||||
|
||||
val engineDependencies
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class AlterationService(
|
|||
}
|
||||
|
||||
@Throws
|
||||
fun save(
|
||||
suspend fun save(
|
||||
json: AlterationJson,
|
||||
create: Boolean,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<List<Alteration>>(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<List<Alteration>> = 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<Alteration> {
|
||||
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<AlterationJson>(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<AlterationJson>(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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Campaign> = 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<CampaignJson>(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<CampaignJson>(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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<List<CharacterSheet>>(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<List<CharacterSheet>> = 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<CharacterSheet> {
|
||||
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<CharacterSheetJson>(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<CharacterSheetJson>(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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<Map<String, Inventory>>(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<Map<String, Inventory>> = 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<String, Inventory> {
|
||||
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<InventoryJson>(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<InventoryJson>(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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<Item>>(emptyList())
|
||||
|
||||
init {
|
||||
val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
scope.launch {
|
||||
updateItemsFlow()
|
||||
}
|
||||
|
|
@ -43,7 +42,7 @@ class ItemStore(
|
|||
|
||||
fun itemsFlow(): StateFlow<List<Item>> = 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<Item> {
|
||||
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<ItemJson>(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<ItemJson>(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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Map<String, TagJson>>(emptyMap())
|
||||
private val characterTagsFlow = MutableStateFlow<Map<String, TagJson>>(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<Map<String, TagJson>> = characterTagsFlow
|
||||
fun itemTags(): StateFlow<Map<String, TagJson>> = 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<Map<String, TagJson>>,
|
||||
file: File,
|
||||
) {
|
||||
|
|
@ -69,7 +68,7 @@ class TagStore(
|
|||
}
|
||||
|
||||
@Throws(FileReadException::class, JsonConversionException::class)
|
||||
private fun File.readTags(): List<TagJson> {
|
||||
private suspend fun File.readTags(): List<TagJson> {
|
||||
// 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<List<TagJson>>(data)
|
||||
jsonSerializer.decodeFromString<List<TagJson>>(data)
|
||||
} catch (exception: Exception) {
|
||||
throw JsonConversionException(
|
||||
root = exception
|
||||
|
|
@ -90,10 +89,10 @@ class TagStore(
|
|||
}
|
||||
|
||||
@Throws(JsonConversionException::class, FileWriteException::class)
|
||||
private fun saveAlterationTags(tags: List<TagJson>) {
|
||||
private suspend fun saveAlterationTags(tags: List<TagJson>) {
|
||||
// convert the data to json format
|
||||
val json = try {
|
||||
this.json.encodeToString(tags)
|
||||
this.jsonSerializer.encodeToString(tags)
|
||||
} catch (exception: Exception) {
|
||||
throw JsonConversionException(root = exception)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<String>
|
||||
get() = sheet.alterations
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ data class APIResponse<T>(
|
|||
Active,
|
||||
Equip,
|
||||
Damage,
|
||||
Armor,
|
||||
Fatigue,
|
||||
Diminished,
|
||||
Count,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -> ' '
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue