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
@ -283,4 +373,9 @@ private fun AlterationEditKeyHandler(
else -> false
}
}
}
}
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
@ -61,4 +63,36 @@ fun GMTag(
text = tag.label,
)
}
}
@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,