diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index 9760b1c..4852c53 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt @@ -304,7 +304,13 @@ private fun MainScaffold( .padding(contentPadding) ) { composable(AppRoutes.CHARACTER_SHEET) { - CharacterSheetPage(character, contentPadding) + CharacterSheetPage( + character = character, + contentPadding = contentPadding, + onUpdateCharacter = { transform -> + characterViewModel.updateCharacter(transform) + } + ) } composable(AppRoutes.SETTINGS) { SettingsPage() diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt index 9b3162b..b95b172 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt @@ -30,6 +30,13 @@ object TestTags { // --- Dice / roll buttons --- fun rollButton(name: String): String = "roll_button_${name.lowercase().replace(" ", "_")}" + // --- Attribute edit dialog --- + const val ATTRIBUTE_EDIT_DIALOG = "attribute_edit_dialog" + const val ATTRIBUTE_EDIT_INPUT = "attribute_edit_input" + const val ATTRIBUTE_EDIT_CONFIRM = "attribute_edit_confirm" + const val ATTRIBUTE_EDIT_DISMISS = "attribute_edit_dismiss" + const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error" + // --- Dice roll result dialog --- const val DICE_ROLL_DIALOG = "dice_roll_dialog" const val DICE_ROLL_DICE_COUNT = "dice_roll_dice_count" diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt index f409ae2..9ded6bd 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt @@ -10,8 +10,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute +import org.shahondin1624.lib.components.charactermodel.attributespage.AttributeEditDialog import org.shahondin1624.lib.components.charactermodel.attributespage.Talent import org.shahondin1624.lib.functions.DiceRoll +import org.shahondin1624.model.attributes.AttributeType import org.shahondin1624.model.charactermodel.ShadowrunCharacter import org.shahondin1624.theme.LocalWindowSizeClass import org.shahondin1624.theme.WindowSizeClass @@ -31,7 +33,11 @@ private enum class CharacterTab(val title: String) { * Replaces the previous single-scroll layout with fast tab switching. */ @Composable -fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { +fun CharacterSheetPage( + character: ShadowrunCharacter, + contentPadding: Dp, + onUpdateCharacter: ((ShadowrunCharacter) -> ShadowrunCharacter) -> Unit = {} +) { val windowSizeClass = LocalWindowSizeClass.current var selectedTab by remember { mutableStateOf(CharacterTab.Overview) } val spacing = UiConstants.Spacing.medium(windowSizeClass) @@ -45,6 +51,9 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { pendingRollLabel = label } + // Attribute edit dialog state + var editingAttributeType by remember { mutableStateOf(null) } + // Show dice roll result dialog pendingRoll?.let { roll -> DiceRollResultDialog( @@ -54,6 +63,22 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { ) } + // Show attribute edit dialog + editingAttributeType?.let { attrType -> + val attr = character.attributes.getAttributeByType(attrType) + AttributeEditDialog( + attribute = attr, + onConfirm = { newValue -> + onUpdateCharacter { char -> + val newAttributes = char.attributes.withAttribute(attrType, newValue) + char.copy(attributes = newAttributes) + } + editingAttributeType = null + }, + onDismiss = { editingAttributeType = null } + ) + } + Column(modifier = Modifier.fillMaxSize()) { // Tab row TabRow( @@ -69,12 +94,16 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { } // Tab content + val onEditAttribute: (AttributeType) -> Unit = { type -> + editingAttributeType = type + } + when (windowSizeClass) { WindowSizeClass.Expanded -> { // Expanded: two-column layout for Overview and Combat when (selectedTab) { CharacterTab.Overview -> ExpandedOverviewContent(character, spacing) - CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll) + CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute) CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll) CharacterTab.Combat -> CombatContent(character, spacing) } @@ -82,7 +111,7 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { else -> { when (selectedTab) { CharacterTab.Overview -> OverviewContent(character, spacing) - CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll) + CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute) CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll) CharacterTab.Combat -> CombatContent(character, spacing) } @@ -134,7 +163,8 @@ private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp) private fun AttributesContent( character: ShadowrunCharacter, spacing: Dp, - onDiceRoll: (DiceRoll, String) -> Unit + onDiceRoll: (DiceRoll, String) -> Unit, + onEditAttribute: (AttributeType) -> Unit ) { val windowSizeClass = LocalWindowSizeClass.current val columns = UiConstants.Grid.totalColumns(windowSizeClass) @@ -157,7 +187,8 @@ private fun AttributesContent( Box(modifier = Modifier.weight(1f)) { Attribute( attribute = attr, - onRoll = { roll -> onDiceRoll(roll, attr.type.name) } + onRoll = { roll -> onDiceRoll(roll, attr.type.name) }, + onEdit = { onEditAttribute(attr.type) } ) } } diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Attribute.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Attribute.kt index c97339a..c28acc9 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Attribute.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Attribute.kt @@ -2,6 +2,7 @@ package org.shahondin1624.lib.components.charactermodel.attributespage import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -35,10 +36,18 @@ fun Attribute( onRoll: (DiceRoll) -> Unit = { println("Result for $attribute: ${it.result}") }, + onEdit: (() -> Unit)? = null, ) { var isInDarkMode by LocalThemeIsDark.current val textColor = if (isInDarkMode) Color.Black else Color.White - Card(modifier = Modifier.testTag(TestTags.attributeCard(attribute.type.name))) { + Card( + modifier = Modifier + .testTag(TestTags.attributeCard(attribute.type.name)) + .then( + if (onEdit != null) Modifier.clickable { onEdit() } + else Modifier + ) + ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributeEditDialog.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributeEditDialog.kt new file mode 100644 index 0000000..1c5dc34 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/AttributeEditDialog.kt @@ -0,0 +1,90 @@ +package org.shahondin1624.lib.components.charactermodel.attributespage + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import org.shahondin1624.lib.components.TestTags +import org.shahondin1624.model.attributes.Attribute + +/** + * Dialog for editing an attribute value. + * Provides a number input with validation for minimum 1 and configurable maximum. + */ +@Composable +fun AttributeEditDialog( + attribute: Attribute, + maxValue: Int = 10, + onConfirm: (Int) -> Unit, + onDismiss: () -> Unit +) { + var textValue by remember { mutableStateOf(attribute.value.toString()) } + val parsedValue = textValue.toIntOrNull() + val isValid = parsedValue != null && parsedValue in 1..maxValue + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.testTag(TestTags.ATTRIBUTE_EDIT_DIALOG), + title = { + Text(text = "Edit ${attribute.type.name}") + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = textValue, + onValueChange = { newValue -> + // Allow only digits + if (newValue.all { it.isDigit() } || newValue.isEmpty()) { + textValue = newValue + } + }, + label = { Text("Value (1-$maxValue)") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + isError = !isValid && textValue.isNotEmpty(), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.ATTRIBUTE_EDIT_INPUT) + ) + if (!isValid && textValue.isNotEmpty()) { + Text( + text = if (parsedValue != null && parsedValue < 1) { + "Minimum value is 1" + } else if (parsedValue != null && parsedValue > maxValue) { + "Maximum value is $maxValue" + } else { + "Enter a valid number" + }, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.testTag(TestTags.ATTRIBUTE_EDIT_ERROR) + ) + } + } + }, + confirmButton = { + TextButton( + onClick = { if (isValid) onConfirm(parsedValue!!) }, + enabled = isValid, + modifier = Modifier.testTag(TestTags.ATTRIBUTE_EDIT_CONFIRM) + ) { + Text("Save") + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + modifier = Modifier.testTag(TestTags.ATTRIBUTE_EDIT_DISMISS) + ) { + Text("Cancel") + } + } + ) +} diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/attributes/Attributes.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/attributes/Attributes.kt index e300882..398efc4 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/attributes/Attributes.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/attributes/Attributes.kt @@ -121,4 +121,21 @@ data class Attributes( fun getAllAttributes(): List { return listOf(body, agility, reaction, strength, willpower, logic, intuition, charisma) } + + /** + * Returns a copy of this Attributes with the specified attribute type set to a new value. + */ + fun withAttribute(type: AttributeType, newValue: Int): Attributes { + val attr = Attribute(type, newValue) + return when (type) { + AttributeType.Body -> copy(body = attr) + AttributeType.Agility -> copy(agility = attr) + AttributeType.Reaction -> copy(reaction = attr) + AttributeType.Strength -> copy(strength = attr) + AttributeType.Willpower -> copy(willpower = attr) + AttributeType.Logic -> copy(logic = attr) + AttributeType.Intuition -> copy(intuition = attr) + AttributeType.Charisma -> copy(charisma = attr) + } + } } \ No newline at end of file