This commit was merged in pull request #63.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<AttributeType?>(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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -121,4 +121,21 @@ data class Attributes(
|
||||
fun getAllAttributes(): List<Attribute> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user