Add specific alteration error management.

This commit is contained in:
Thomas Andres Gomez 2025-04-01 22:18:55 +02:00
parent 6213d5ac15
commit f94a530621
48 changed files with 606 additions and 511 deletions

View file

@ -242,6 +242,7 @@
<string name="game_master__character_action__add_to_npc">Ajouter aux Npcs</string>
<string name="game_master__character_action__remove_from_npc">Retirer des Npcs</string>
<string name="game_master__create_character_sheet">Créer un personnage</string>
<string name="game_master__alteration__title">Édition d'Altération</string>
<string name="game_master__alteration__filter">Filtrer par nom :</string>
<string name="game_master__alteration__create">Créer une altération</string>
<string name="game_master__alteration__delete">Supprimer l'altération</string>

View file

@ -4,10 +4,12 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse
class LwaNetworkException(
val status: Int,
val code: APIResponse.ErrorCode?,
message: String,
) : Exception(message) {
constructor(error: APIResponse<*>) : this(
status = error.status,
code = error.code,
message = error.message ?: "An unknown error occurred"
)
}

View file

@ -7,6 +7,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import com.pixelized.desktop.lwa.LocalErrorSnackHost
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.error__default__action
import org.jetbrains.compose.resources.getString
@Stable
class ErrorSnackUio(
@ -15,10 +19,10 @@ class ErrorSnackUio(
val duration: SnackbarDuration,
) {
companion object {
fun from(exception: Exception) = ErrorSnackUio(
suspend fun from(exception: Exception) = ErrorSnackUio(
message = exception.localizedMessage,
action = "Ok",
duration = SnackbarDuration.Indefinite
action = getString(Res.string.error__default__action),
duration = SnackbarDuration.Long,
)
}
}
@ -29,7 +33,7 @@ fun ErrorSnackHandler(
error: SharedFlow<ErrorSnackUio>,
) {
LaunchedEffect(Unit) {
error.collect {
error.collectLatest {
snack.showSnackbar(
message = it.message,
actionLabel = it.action,

View file

@ -1,6 +1,7 @@
package com.pixelized.desktop.lwa.ui.composable.textfield
import androidx.compose.foundation.layout.height
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldColors
@ -9,20 +10,21 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaTextFieldColors
import com.pixelized.desktop.lwa.utils.rememberKeyboardActions
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
data class LwaTextFieldUio(
val enable: Boolean = true,
val isError: StateFlow<Boolean>,
val labelFlow: StateFlow<String?>?,
val valueFlow: StateFlow<String>,
val placeHolderFlow: StateFlow<String?>?,
val isError: MutableStateFlow<Boolean>,
val labelFlow: MutableStateFlow<String?>?,
val valueFlow: MutableStateFlow<String>,
val placeHolderFlow: MutableStateFlow<String?>?,
val onValueChange: (String) -> Unit,
)
@ -30,6 +32,7 @@ data class LwaTextFieldUio(
fun LwaTextField(
modifier: Modifier = Modifier,
colors: TextFieldColors = LwaTextFieldColors(),
shape: Shape = MaterialTheme.shapes.small,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
singleLine: Boolean = true,
@ -51,6 +54,7 @@ fun LwaTextField(
TextField(
modifier = localModifier.then(other = modifier),
colors = colors,
shape = shape,
keyboardActions = rememberKeyboardActions {
focus.moveFocus(FocusDirection.Next)
},

View file

@ -7,7 +7,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditPage
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditScreen
import com.pixelized.desktop.lwa.utils.extention.ARG
@Stable
@ -44,7 +44,7 @@ fun NavGraphBuilder.composableGameMasterAlterationEditPage() {
route = GMAlterationEditDestination.baseRoute(),
arguments = GMAlterationEditDestination.arguments(),
) {
GMAlterationEditPage()
GMAlterationEditScreen()
}
}

View file

@ -1,34 +1,39 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory
import com.pixelized.shared.lwa.model.alteration.Alteration
import com.pixelized.shared.lwa.model.tag.Tag
import com.pixelized.shared.lwa.parser.expression.ExpressionParser
import kotlinx.coroutines.flow.MutableStateFlow
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_description
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_expression
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_id
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_id
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_label
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_description
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_tags
import org.jetbrains.compose.resources.getString
import java.util.UUID
class GMAlterationEditFactory(
private val expressionParser: ExpressionParser,
private val tagFactory: GMTagFactory,
) {
suspend fun createForm(
originId: String?,
alteration: Alteration?,
tags: Collection<Tag>,
): GMAlterationEditPageUio {
val id = MutableStateFlow(alteration?.id ?: "")
val label = MutableStateFlow(alteration?.metadata?.name ?: "")
val description = MutableStateFlow(alteration?.metadata?.description ?: "")
val tags = MutableStateFlow(alteration?.tags?.joinToString(", ") { it } ?: "")
val fields = MutableStateFlow(alteration?.fields?.map { createField(it) } ?: listOf(createField(null)))
val fields = MutableStateFlow(alteration?.fields?.map { createField(it) } ?: listOf(
createField(null)
))
return GMAlterationEditPageUio(
id = LwaTextFieldUio(
enable = true,
enable = originId == null,
isError = MutableStateFlow(false),
labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_id)),
valueFlow = id,
@ -51,13 +56,9 @@ class GMAlterationEditFactory(
placeHolderFlow = null,
onValueChange = { description.value = it },
),
tags = LwaTextFieldUio(
enable = true,
isError = MutableStateFlow(false),
labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_tags)),
valueFlow = tags,
placeHolderFlow = null,
onValueChange = { tags.value = it },
tags = tagFactory.convertToGMTagItemUio(
tags = tags,
selectedTagIds = alteration?.tags ?: emptyList(),
),
fields = fields,
)
@ -101,7 +102,7 @@ class GMAlterationEditFactory(
name = form.label.valueFlow.value,
description = form.description.valueFlow.value,
),
tags = form.tags.valueFlow.value.split(","),
tags = form.tags.filter { it.highlight }.map { it.id },
fields = form.fields.value.mapNotNull { field ->
expressionParser.parse(input = field.expression.valueFlow.value)?.let {
Alteration.Field(

View file

@ -1,9 +1,16 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -11,21 +18,31 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
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.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
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.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@ -34,21 +51,27 @@ 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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler
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.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.screen.gamemaster.LocalGMScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMAlterationEditDestination
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagButton
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_add_field
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_cancel
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_save
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__title
import lwacharactersheet.composeapp.generated.resources.ic_save_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@ -59,7 +82,7 @@ data class GMAlterationEditPageUio(
val id: LwaTextFieldUio,
val label: LwaTextFieldUio,
val description: LwaTextFieldUio,
val tags: LwaTextFieldUio,
val tags: List<GMTagUio>,
val fields: MutableStateFlow<List<SkillUio>>,
) {
@Stable
@ -76,40 +99,37 @@ object GMAlterationEditPageDefault {
}
@Composable
fun GMAlterationEditPage(
fun GMAlterationEditScreen(
viewModel: GMAlterationEditViewModel = koinViewModel(),
) {
val screen = LocalScreenController.current
val scope = rememberCoroutineScope()
val form = viewModel.form.collectAsState()
AnimatedContent(
targetState = form.value,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) {
when (it) {
null -> Box(modifier = Modifier.fillMaxSize())
else -> GMAlterationEditContent(
modifier = Modifier.fillMaxSize(),
form = it,
paddings = GMAlterationEditPageDefault.paddings,
addField = {
scope.launch {
viewModel.addField()
}
},
removeField = viewModel::removeField,
onSave = {
scope.launch {
viewModel.save()
}
},
onCancel = {
screen.popBackStack()
},
)
}
}
GMAlterationEditContent(
modifier = Modifier.fillMaxSize(),
form = form,
paddings = GMAlterationEditPageDefault.paddings,
onBack = {
screen.navigateBack()
},
addField = {
scope.launch {
viewModel.addField()
}
},
removeField = viewModel::removeField,
onSave = {
scope.launch {
if (viewModel.save()) {
screen.navigateBack()
}
}
},
onTag = { tag ->
viewModel.addTag(tag = tag)
},
)
ErrorSnackHandler(
error = viewModel.error,
@ -117,7 +137,7 @@ fun GMAlterationEditPage(
AlterationEditKeyHandler(
onDismissRequest = {
screen.popBackStack()
screen.navigateBack()
}
)
}
@ -125,148 +145,218 @@ fun GMAlterationEditPage(
@Composable
private fun GMAlterationEditContent(
modifier: Modifier = Modifier,
form: GMAlterationEditPageUio,
scope: CoroutineScope = rememberCoroutineScope(),
tagsState: LazyListState = rememberLazyListState(),
form: State<GMAlterationEditPageUio?>,
paddings: PaddingValues,
onBack: () -> Unit,
addField: () -> Unit,
removeField: (index: Int) -> Unit,
onSave: () -> Unit,
onCancel: () -> Unit,
onTag: (GMTagUio) -> Unit,
) {
val fields = form.fields.collectAsState()
LazyColumn(
Scaffold(
modifier = modifier,
contentPadding = paddings,
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
item(
key = "Id",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = form.id,
singleLine = true,
)
}
item(
key = "Name",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = form.label,
singleLine = true,
)
}
item(
key = "Description",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = form.description,
singleLine = false,
)
}
item(
key = "Tags",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = form.tags,
singleLine = true,
)
}
itemsIndexed(
items = fields.value,
key = { _, item -> item.key },
) { index, item ->
Row(
modifier = Modifier.animateItem(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
LwaTextField(
modifier = Modifier.weight(1f),
field = item.id,
singleLine = true,
)
LwaTextField(
modifier = Modifier.weight(1f),
field = item.expression,
singleLine = true,
)
IconButton(
onClick = { removeField(index) },
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = MaterialTheme.lwa.colorScheme.base.primary,
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(Res.string.game_master__alteration__title),
)
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = null,
)
}
},
actions = {
TextButton(
onClick = onSave,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.SemiBold,
text = stringResource(Res.string.game_master__alteration__edit_field_save),
)
Icon(
painter = painterResource(Res.drawable.ic_save_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
)
},
content = {
AnimatedContent(
targetState = form.value,
transitionSpec = {
if (initialState?.id == targetState?.id) {
EnterTransition.None togetherWith ExitTransition.None
} else {
fadeIn() togetherWith fadeOut()
}
}
) {
when (it) {
null -> Box(
modifier = Modifier.fillMaxSize(),
)
else -> {
val fields = it.fields.collectAsState()
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = paddings,
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
item(
key = "Id",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = it.id,
singleLine = true,
)
}
item(
key = "Name",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = it.label,
singleLine = true,
)
}
item(
key = "Description",
) {
LwaTextField(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
field = it.description,
singleLine = false,
)
}
item(
key = "Tags",
) {
LazyRow(
modifier = Modifier.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
scope.launch {
tagsState.scrollBy(-delta)
}
},
),
state = tagsState,
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
items(
items = it.tags,
) { tag ->
GMTagButton(
modifier = Modifier.height(48.dp),
tag = tag,
onTag = { onTag(tag) }
)
}
}
}
itemsIndexed(
items = fields.value,
key = { _, item -> item.key },
) { index, item ->
Row(
modifier = Modifier.animateItem(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
LwaTextField(
modifier = Modifier.weight(1f),
field = item.id,
singleLine = true,
)
LwaTextField(
modifier = Modifier.weight(1f),
field = item.expression,
singleLine = true,
)
IconButton(
modifier = Modifier
.size(size = 56.dp)
.background(
color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
shape = MaterialTheme.shapes.small,
),
onClick = { removeField(index) },
) {
Icon(
imageVector = Icons.Default.Close,
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
}
item(
key = "Actions",
) {
Column(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = addField,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__alteration__edit_add_field),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = onSave,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__alteration__edit_field_save),
)
Icon(
painter = painterResource(Res.drawable.ic_save_24dp),
contentDescription = null,
)
}
}
}
}
}
}
}
}
item(
key = "Actions",
) {
Column(
modifier = Modifier
.animateItem()
.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = addField,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__alteration__edit_add_field),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = onSave,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__alteration__edit_field_save),
)
Icon(
painter = painterResource(Res.drawable.ic_save_24dp),
contentDescription = null,
)
}
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = onCancel,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__alteration__edit_field_cancel),
)
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
}
}
)
}
@Composable
@ -284,3 +374,8 @@ private fun AlterationEditKeyHandler(
}
}
}
private fun NavHostController.navigateBack() = popBackStack(
route = GMAlterationEditDestination.baseRoute(),
inclusive = true,
)

View file

@ -3,9 +3,13 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.network.LwaNetworkException
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.tag.TagRepository
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMAlterationEditDestination
import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio
import com.pixelized.shared.lwa.protocol.rest.APIResponse.ErrorCode
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@ -15,6 +19,7 @@ import kotlinx.coroutines.launch
class GMAlterationEditViewModel(
private val alterationRepository: AlterationRepository,
private val tagRepository: TagRepository,
private val factory: GMAlterationEditFactory,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
@ -29,28 +34,33 @@ class GMAlterationEditViewModel(
init {
viewModelScope.launch {
_form.value = factory.createForm(
alteration = alterationRepository.alteration(alterationId = argument.id)
originId = argument.id,
alteration = alterationRepository.alteration(alterationId = argument.id),
tags = tagRepository.alterationsTagFlow().value.values,
)
}
}
suspend fun save() {
val edited = factory.createAlteration(form = form.value)
val actual = alterationRepository.alterationFlow.value[edited?.id]
if (edited == null) return
suspend fun save(): Boolean {
val edited = factory.createAlteration(form = form.value) ?: return false
try {
if (argument.id == null && actual?.id != null) {
error("Id already taken by an another alteration")
}
alterationRepository.updateAlteration(
alteration = edited,
create = argument.id == null,
)
return true
} catch (exception: LwaNetworkException) {
_form.value?.id?.isError?.value = exception.code == ErrorCode.AlterationId
_form.value?.label?.isError?.value = exception.code == ErrorCode.AlterationName
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
return false
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception = exception)
_error.emit(message)
return false
}
}
@ -69,4 +79,16 @@ class GMAlterationEditViewModel(
}
}
}
fun addTag(tag: GMTagUio) {
_form.update {
it?.copy(
tags = it.tags.toMutableList().also { tags ->
val index = tags.indexOf(tag)
if (index > -1)
tags[index] = tag.copy(highlight = tag.highlight.not())
}
)
}
}
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
@ -15,6 +16,7 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
import com.pixelized.desktop.lwa.ui.theme.lwa
@ -62,3 +64,35 @@ fun GMTag(
)
}
}
@Composable
fun GMTagButton(
modifier: Modifier = Modifier,
padding: PaddingValues = GmTagDefault.padding,
shape: Shape = MaterialTheme.shapes.small,
tag: GMTagUio,
onTag: (() -> Unit)? = null,
) {
val animatedColor = animateColorAsState(
when (tag.highlight) {
true -> MaterialTheme.lwa.colorScheme.base.secondary
else -> MaterialTheme.lwa.colorScheme.base.onSurface
}
)
Button(
modifier = modifier,
colors = LwaButtonColors(contentColor = animatedColor.value),
shape = shape,
enabled = onTag != null,
onClick = { onTag?.invoke() },
) {
Text(
modifier = Modifier.padding(paddingValues = padding),
color = animatedColor.value,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = tag.label,
)
}
}

View file

@ -5,6 +5,23 @@ import java.text.Collator
class GMTagFactory {
fun convertToGMTagItemUio(
tags: Collection<Tag>,
selectedTagIds: List<String>,
): List<GMTagUio> {
return tags
.map { tag ->
GMTagUio(
id = tag.id,
label = tag.label,
highlight = selectedTagIds.contains(tag.id),
)
}
.sortedWith(
comparator = compareBy(Collator.getInstance()) { it.label }
)
}
fun convertToGMTagItemUio(
tags: Collection<Tag>,
selectedTagId: String?,

View file

@ -56,7 +56,7 @@ fun LwaTheme(
MaterialTheme(
colors = lwaColors.base,
typography = MaterialTheme.typography,
shapes = MaterialTheme.shapes,
shapes = lwaShapes.base,
content = content,
)
}

View file

@ -6,12 +6,14 @@ import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
@Composable
@Stable
fun LwaButtonColors(): ButtonColors = ButtonDefaults.buttonColors(
fun LwaButtonColors(
contentColor: Color = MaterialTheme.colors.primary,
): ButtonColors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.primary,
contentColor = contentColor,
disabledContentColor = MaterialTheme.colors.surface.copy(alpha = ContentAlpha.disabled),
)

View file

@ -1,6 +1,7 @@
package com.pixelized.desktop.lwa.ui.theme.shapes
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
@ -9,6 +10,7 @@ import androidx.compose.ui.unit.dp
@Stable
data class LwaShapes(
val base: Shapes,
val portrait: Shape,
val panel: Shape,
val settings: Shape,
@ -18,12 +20,16 @@ data class LwaShapes(
@Stable
@Composable
fun lwaShapes(
base: Shapes = Shapes(
small = RoundedCornerShape(4.dp),
),
portrait: Shape = RoundedCornerShape(8.dp),
panel: Shape = RoundedCornerShape(8.dp),
settings: Shape = RoundedCornerShape(8.dp),
gameMaster: Shape = RoundedCornerShape(8.dp),
): LwaShapes = remember {
LwaShapes(
base = base,
portrait = portrait,
panel = panel,
settings = settings,

View file

@ -1,8 +1,14 @@
package com.pixelized.server.lwa.model.alteration
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
import com.pixelized.server.lwa.server.exception.JsonCodingException
import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.alteration.Alteration
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -93,8 +99,9 @@ class AlterationStore(
val file = alterationFile(id = json.id)
// Guard case on update alteration
if (create && file.exists()) {
val root = Exception("Alteration already exist, creation is impossible.")
throw BusinessException(root = root)
throw BusinessException(
message = "Alteration already exist, creation is impossible.",
)
}
// Transform the json into the model.
val alteration = try {
@ -102,6 +109,18 @@ class AlterationStore(
} 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)
@ -139,13 +158,15 @@ class AlterationStore(
val file = alterationFile(id = id)
// Guard case on the file existence.
if (file.exists().not()) {
val root = Exception("Alteration doesn't not exist, deletion is impossible.")
throw BusinessException(root = root)
throw BusinessException(
message = "Alteration doesn't not exist, deletion is impossible.",
)
}
// Guard case on the file deletion
if (file.delete().not()) {
val root = Exception("Alteration file have not been deleted for unknown reason.")
throw BusinessException(root = root)
throw BusinessException(
message = "Alteration file have not been deleted for unknown reason.",
)
}
// Update the data model with the deleted alteration.
alterationFlow.update { alterations ->
@ -162,11 +183,4 @@ class AlterationStore(
private fun alterationFile(id: String): File {
return File("${pathProvider.alterationsPath()}${id}.json")
}
sealed class AlterationStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : AlterationStoreException(root)
class JsonCodingException(root: Exception) : AlterationStoreException(root)
class BusinessException(root: Exception) : AlterationStoreException(root)
class FileWriteException(root: Exception) : AlterationStoreException(root)
class FileReadException(root: Exception) : AlterationStoreException(root)
}

View file

@ -1,5 +1,6 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
@ -43,8 +44,7 @@ class CampaignService(
) {
// Check if the character is already in the campaign.
if (campaign.instances.contains(characterSheetId)) {
val root = Exception("Character with id:$characterSheetId is already in the campaign.")
throw CampaignStore.BusinessException(root = root)
throw BusinessException(message = "Character with id:$characterSheetId is already in the campaign.")
}
// Update the corresponding instance
val characters = campaign.characters.toMutableSet().also {
@ -62,8 +62,7 @@ class CampaignService(
) {
// Check if the character is already in the campaign.
if (campaign.instances.contains(characterSheetId)) {
val root = Exception("Character with id:$characterSheetId is already in the campaign.")
throw CampaignStore.BusinessException(root = root)
throw BusinessException(message = "Character with id:$characterSheetId is already in the campaign.")
}
// Update the corresponding instance
val characters = campaign.npcs.toMutableSet().also {
@ -81,8 +80,7 @@ class CampaignService(
) {
// Check if the character is in the campaign.
if (campaign.characters.contains(characterSheetId).not()) {
val root = Exception("Character with id:$characterSheetId is not in the party.")
throw CampaignStore.BusinessException(root = root)
throw BusinessException(message = "Character with id:$characterSheetId is not in the party.")
}
// Update the corresponding instance
val characters = campaign.characters.toMutableSet().also {
@ -100,8 +98,7 @@ class CampaignService(
) {
// Check if the character is in the campaign.
if (campaign.npcs.contains(characterSheetId).not()) {
val root = Exception("Character with id:$characterSheetId is not in the npcs.")
throw CampaignStore.BusinessException(root = root)
throw BusinessException(message = "Character with id:$characterSheetId is not in the npcs.")
}
// Update the corresponding instance
val characters = campaign.npcs.toMutableSet().also {

View file

@ -1,5 +1,9 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
import com.pixelized.server.lwa.server.exception.JsonCodingException
import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
@ -108,11 +112,4 @@ class CampaignStore(
private fun campaignFile(): File {
return File("${pathProvider.campaignPath()}campaign.json")
}
sealed class CampaignStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CampaignStoreException(root)
class JsonCodingException(root: Exception) : CampaignStoreException(root)
class BusinessException(root: Exception) : CampaignStoreException(root)
class FileWriteException(root: Exception) : CampaignStoreException(root)
class FileReadException(root: Exception) : CampaignStoreException(root)
}

View file

@ -1,8 +1,14 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
import com.pixelized.server.lwa.server.exception.JsonCodingException
import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -93,8 +99,7 @@ class CharacterSheetStore(
val file = characterSheetFile(id = sheet.id)
// Guard case on update alteration
if (create && file.exists()) {
val root = Exception("Character already exist, creation is impossible.")
throw BusinessException(root = root)
throw BusinessException(message = "Character already exist, creation is impossible.")
}
// Transform the json into the model.
val json = try {
@ -139,13 +144,16 @@ class CharacterSheetStore(
val file = characterSheetFile(id = characterSheetId)
// Guard case on the file existence.
if (file.exists().not()) {
val root = Exception("Character file with id:$characterSheetId doesn't not exist.")
throw BusinessException(root = root)
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()) {
val root = Exception("Character file have not been deleted for unknown reason.")
throw BusinessException(root = root)
throw BusinessException(
message = "Character file have not been deleted for unknown reason.",
)
}
// Update the data model with the deleted character.
characterSheetsFlow.update { characters ->
@ -158,11 +166,4 @@ class CharacterSheetStore(
private fun characterSheetFile(id: String): File {
return File("${pathProvider.characterStorePath()}${id}.json")
}
sealed class CharacterSheetStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CharacterSheetStoreException(root)
class JsonCodingException(root: Exception) : CharacterSheetStoreException(root)
class BusinessException(root: Exception) : CharacterSheetStoreException(root)
class FileWriteException(root: Exception) : CharacterSheetStoreException(root)
class FileReadException(root: Exception) : CharacterSheetStoreException(root)
}

View file

@ -1,5 +1,8 @@
package com.pixelized.server.lwa.model.tag
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.tag.TagJson
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
@ -100,9 +103,4 @@ class TagStore(
private fun characterFile() = File("${pathProvider.tagsPath()}$CHARACTER.json")
private fun alterationFile() = File("${pathProvider.tagsPath()}$ALTERATION.json")
sealed class TagStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : TagStoreException(root)
class FileWriteException(root: Exception) : TagStoreException(root)
class FileReadException(root: Exception) : TagStoreException(root)
}

View file

@ -0,0 +1,10 @@
package com.pixelized.server.lwa.server.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
class BusinessException(
message: String,
val code: APIResponse.ErrorCode? = null,
) : ServerException(
root = Exception(message)
)

View file

@ -0,0 +1,3 @@
package com.pixelized.server.lwa.server.exception
class FileReadException(root: Exception) : ServerException(root)

View file

@ -0,0 +1,3 @@
package com.pixelized.server.lwa.server.exception
class FileWriteException(root: Exception) : ServerException(root)

View file

@ -0,0 +1,3 @@
package com.pixelized.server.lwa.server.exception
class JsonCodingException(root: Exception) : ServerException(root)

View file

@ -0,0 +1,3 @@
package com.pixelized.server.lwa.server.exception
class JsonConversionException(root: Exception) : ServerException(root)

View file

@ -0,0 +1,4 @@
package com.pixelized.server.lwa.server.exception
class MissingParameterException(name: String) :
ServerException(root = Exception("Missing '$name' parameter."))

View file

@ -0,0 +1,3 @@
package com.pixelized.server.lwa.server.exception
sealed class ServerException(root: Exception) : Exception(root)

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.alterationId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
@ -27,19 +27,9 @@ fun Engine.deleteAlteration(): suspend RoutingContext.() -> Unit {
alterationId = alterationId,
),
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception,
)
}
}

View file

@ -2,6 +2,7 @@ package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.alterationId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -22,11 +23,8 @@ fun Engine.getAlteration(): suspend RoutingContext.() -> Unit {
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception,
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -14,11 +15,8 @@ fun Engine.getAlterationTags(): suspend RoutingContext.() -> Unit {
),
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception,
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -14,11 +15,8 @@ fun Engine.getAlterations(): suspend RoutingContext.() -> Unit {
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception,
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.create
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
@ -29,19 +29,9 @@ fun Engine.putAlteration(): suspend RoutingContext.() -> Unit {
alterationId = form.id,
),
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception,
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.server.response.respond
@ -27,19 +27,9 @@ fun Engine.removeCampaignCharacter(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.server.response.respond
@ -27,19 +27,9 @@ fun Engine.removeCampaignNpc(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -14,11 +15,8 @@ fun Engine.getCampaign(): suspend RoutingContext.() -> Unit {
),
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.server.response.respond
@ -27,19 +27,9 @@ fun Engine.putCampaignCharacter(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
import io.ktor.server.response.respond
@ -27,19 +27,9 @@ fun Engine.putCampaignNpc(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,7 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV2
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent
@ -30,19 +30,9 @@ fun Engine.putCampaignScene(): suspend RoutingContext.() -> Unit {
name = scene.name,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
@ -27,19 +27,9 @@ fun Engine.deleteCharacter(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId,
),
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -22,19 +22,9 @@ fun Engine.getCharacter(): suspend RoutingContext.() -> Unit {
data = characterSheet,
),
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -14,11 +15,8 @@ fun Engine.getCharacterTags(): suspend RoutingContext.() -> Unit {
),
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
@ -14,11 +15,8 @@ fun Engine.getCharacters(): suspend RoutingContext.() -> Unit {
),
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,8 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.create
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
@ -28,19 +28,9 @@ fun Engine.putCharacter(): suspend RoutingContext.() -> Unit {
characterSheetId = form.id,
),
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,10 +1,10 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.active
import com.pixelized.server.lwa.utils.extentions.alterationId
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -35,19 +35,9 @@ fun Engine.putCharacterAlteration(): suspend RoutingContext.() -> Unit {
active = active,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,9 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -14,10 +15,7 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val damage = call.queryParameters["damage"]?.toIntOrNull()
?: throw MissingParameterException(
name = "damage",
errorCode = APIResponse.MISSING_DAMAGE
)
?: throw MissingParameterException(name = "damage")
// fetch the character sheet
val characterSheet = characterService.character(characterSheetId)
?: error("CharacterSheet with id:$characterSheetId not found.")
@ -38,19 +36,9 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit {
damage = damage,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,9 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -14,10 +15,7 @@ fun Engine.putCharacterDiminished(): suspend RoutingContext.() -> Unit {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val diminished = call.queryParameters["diminished"]?.toIntOrNull()
?: throw MissingParameterException(
name = "diminished",
errorCode = APIResponse.MISSING_DIMINISHED
)
?: throw MissingParameterException(name = "diminished")
// Update the character damage
characterService.updateDiminished(
characterSheetId = characterSheetId,
@ -34,19 +32,9 @@ fun Engine.putCharacterDiminished(): suspend RoutingContext.() -> Unit {
diminished = diminished,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,8 +1,9 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.MissingParameterException
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import io.ktor.server.response.respond
@ -14,10 +15,7 @@ fun Engine.putCharacterFatigue(): suspend RoutingContext.() -> Unit {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val fatigue = call.queryParameters["fatigue"]?.toIntOrNull()
?: throw MissingParameterException(
name = "fatigue",
errorCode = APIResponse.MISSING_FATIGUE
)
?: throw MissingParameterException(name = "fatigue")
// fetch the character sheet
val characterSheet = characterService.character(characterSheetId)
?: error("CharacterSheet with id:$characterSheetId not found.")
@ -38,19 +36,9 @@ fun Engine.putCharacterFatigue(): suspend RoutingContext.() -> Unit {
fatigue = fatigue,
)
)
} catch (exception: MissingParameterException) {
call.respond(
message = APIResponse.error(
status = exception.errorCode,
message = exception.message ?: "?",
)
)
} catch (exception: Exception) {
call.respond(
message = APIResponse.error(
status = APIResponse.GENERIC,
message = exception.message ?: "?",
)
call.exception(
exception = exception
)
}
}

View file

@ -1,41 +1,24 @@
package com.pixelized.server.lwa.utils.extentions
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.server.lwa.server.exception.MissingParameterException
import io.ktor.http.Parameters
val Parameters.characterSheetId
get() = "characterSheetId".let { param ->
this[param] ?: throw MissingParameterException(
name = param,
errorCode = APIResponse.MISSING_CHARACTER_SHEET_ID,
)
this[param] ?: throw MissingParameterException(name = param)
}
val Parameters.alterationId
get() = "alterationId".let { param ->
this[param] ?: throw MissingParameterException(
name = param,
errorCode = APIResponse.MISSING_ALTERATION_ID,
)
this[param] ?: throw MissingParameterException(name = param)
}
val Parameters.create
get() = "create".let { param ->
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(
name = param,
errorCode = APIResponse.MISSING_CREATE
)
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = param)
}
val Parameters.active
get() = "active".let { param ->
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(
name = param,
errorCode = APIResponse.MISSING_ACTIVE
)
this[param]?.toBooleanStrictOrNull() ?: throw MissingParameterException(name = param)
}
class MissingParameterException(
name: String,
val errorCode: Int,
) : Exception("Missing $name parameter.")

View file

@ -0,0 +1,40 @@
package com.pixelized.server.lwa.utils.extentions
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.MissingParameterException
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingCall
suspend inline fun <reified T : Exception> RoutingCall.exception(exception: T) {
when (exception) {
is MissingParameterException -> {
respond(
message = APIResponse.error(
status = APIResponse.BAD_REQUEST,
message = exception.message ?: "?",
code = APIResponse.ErrorCode.AlterationName,
)
)
}
is BusinessException -> {
respond(
message = APIResponse.error(
status = APIResponse.INTERNAL_ERROR,
message = exception.message ?: "?",
code = exception.code,
)
)
}
else -> respond(
message = APIResponse.error(
status = APIResponse.INTERNAL_ERROR,
message = exception.message ?: "?",
)
)
}
}

View file

@ -7,46 +7,52 @@ data class APIResponse<T>(
val success: Boolean,
val status: Int,
val message: String?,
val code: ErrorCode?,
val data: T?,
) {
@Serializable
enum class ErrorCode {
AlterationId,
AlterationName,
CharacterSheetId,
}
companion object {
const val SUCCESS = 100
const val GENERIC = 600
const val MISSING_PARAMETER = 700
const val MISSING_CHARACTER_SHEET_ID = MISSING_PARAMETER + 1
const val MISSING_ALTERATION_ID = MISSING_PARAMETER + 2
const val MISSING_CREATE = MISSING_PARAMETER + 3
const val MISSING_ACTIVE = MISSING_PARAMETER + 4
const val MISSING_DAMAGE = MISSING_PARAMETER + 5
const val MISSING_FATIGUE = MISSING_PARAMETER + 6
const val MISSING_DIMINISHED = MISSING_PARAMETER + 7
fun error(
status: Int,
code: ErrorCode? = null,
message: String?,
) = APIResponse(
success = false,
status = status,
code = code,
message = message,
data = null,
)
fun success() = APIResponse(
fun success(
status: Int = OK,
) = APIResponse(
success = true,
status = SUCCESS,
status = status,
code = null,
message = null,
data = null,
)
inline fun <reified T> success(
status: Int = OK,
data: T? = null,
) = APIResponse(
success = true,
status = SUCCESS,
status = status,
code = null,
message = null,
data = data,
)
const val OK = 200
const val BAD_REQUEST = 400
const val INTERNAL_ERROR = 500
}
}