This commit was merged in pull request #63.
This commit is contained in:
@@ -304,7 +304,13 @@ private fun MainScaffold(
|
|||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
) {
|
) {
|
||||||
composable(AppRoutes.CHARACTER_SHEET) {
|
composable(AppRoutes.CHARACTER_SHEET) {
|
||||||
CharacterSheetPage(character, contentPadding)
|
CharacterSheetPage(
|
||||||
|
character = character,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
onUpdateCharacter = { transform ->
|
||||||
|
characterViewModel.updateCharacter(transform)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(AppRoutes.SETTINGS) {
|
composable(AppRoutes.SETTINGS) {
|
||||||
SettingsPage()
|
SettingsPage()
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ object TestTags {
|
|||||||
// --- Dice / roll buttons ---
|
// --- Dice / roll buttons ---
|
||||||
fun rollButton(name: String): String = "roll_button_${name.lowercase().replace(" ", "_")}"
|
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 ---
|
// --- Dice roll result dialog ---
|
||||||
const val DICE_ROLL_DIALOG = "dice_roll_dialog"
|
const val DICE_ROLL_DIALOG = "dice_roll_dialog"
|
||||||
const val DICE_ROLL_DICE_COUNT = "dice_roll_dice_count"
|
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 androidx.compose.ui.unit.Dp
|
||||||
import org.shahondin1624.lib.components.UiConstants
|
import org.shahondin1624.lib.components.UiConstants
|
||||||
import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute
|
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.components.charactermodel.attributespage.Talent
|
||||||
import org.shahondin1624.lib.functions.DiceRoll
|
import org.shahondin1624.lib.functions.DiceRoll
|
||||||
|
import org.shahondin1624.model.attributes.AttributeType
|
||||||
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
||||||
import org.shahondin1624.theme.LocalWindowSizeClass
|
import org.shahondin1624.theme.LocalWindowSizeClass
|
||||||
import org.shahondin1624.theme.WindowSizeClass
|
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.
|
* Replaces the previous single-scroll layout with fast tab switching.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
|
fun CharacterSheetPage(
|
||||||
|
character: ShadowrunCharacter,
|
||||||
|
contentPadding: Dp,
|
||||||
|
onUpdateCharacter: ((ShadowrunCharacter) -> ShadowrunCharacter) -> Unit = {}
|
||||||
|
) {
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
var selectedTab by remember { mutableStateOf(CharacterTab.Overview) }
|
var selectedTab by remember { mutableStateOf(CharacterTab.Overview) }
|
||||||
val spacing = UiConstants.Spacing.medium(windowSizeClass)
|
val spacing = UiConstants.Spacing.medium(windowSizeClass)
|
||||||
@@ -45,6 +51,9 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
|
|||||||
pendingRollLabel = label
|
pendingRollLabel = label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attribute edit dialog state
|
||||||
|
var editingAttributeType by remember { mutableStateOf<AttributeType?>(null) }
|
||||||
|
|
||||||
// Show dice roll result dialog
|
// Show dice roll result dialog
|
||||||
pendingRoll?.let { roll ->
|
pendingRoll?.let { roll ->
|
||||||
DiceRollResultDialog(
|
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()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Tab row
|
// Tab row
|
||||||
TabRow(
|
TabRow(
|
||||||
@@ -69,12 +94,16 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tab content
|
// Tab content
|
||||||
|
val onEditAttribute: (AttributeType) -> Unit = { type ->
|
||||||
|
editingAttributeType = type
|
||||||
|
}
|
||||||
|
|
||||||
when (windowSizeClass) {
|
when (windowSizeClass) {
|
||||||
WindowSizeClass.Expanded -> {
|
WindowSizeClass.Expanded -> {
|
||||||
// Expanded: two-column layout for Overview and Combat
|
// Expanded: two-column layout for Overview and Combat
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
|
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.Talents -> TalentsContent(character, spacing, onDiceRoll)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
}
|
}
|
||||||
@@ -82,7 +111,7 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
|
|||||||
else -> {
|
else -> {
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> OverviewContent(character, spacing)
|
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.Talents -> TalentsContent(character, spacing, onDiceRoll)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
}
|
}
|
||||||
@@ -134,7 +163,8 @@ private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp)
|
|||||||
private fun AttributesContent(
|
private fun AttributesContent(
|
||||||
character: ShadowrunCharacter,
|
character: ShadowrunCharacter,
|
||||||
spacing: Dp,
|
spacing: Dp,
|
||||||
onDiceRoll: (DiceRoll, String) -> Unit
|
onDiceRoll: (DiceRoll, String) -> Unit,
|
||||||
|
onEditAttribute: (AttributeType) -> Unit
|
||||||
) {
|
) {
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val columns = UiConstants.Grid.totalColumns(windowSizeClass)
|
val columns = UiConstants.Grid.totalColumns(windowSizeClass)
|
||||||
@@ -157,7 +187,8 @@ private fun AttributesContent(
|
|||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
Attribute(
|
Attribute(
|
||||||
attribute = attr,
|
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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -35,10 +36,18 @@ fun Attribute(
|
|||||||
onRoll: (DiceRoll) -> Unit = {
|
onRoll: (DiceRoll) -> Unit = {
|
||||||
println("Result for $attribute: ${it.result}")
|
println("Result for $attribute: ${it.result}")
|
||||||
},
|
},
|
||||||
|
onEdit: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var isInDarkMode by LocalThemeIsDark.current
|
var isInDarkMode by LocalThemeIsDark.current
|
||||||
val textColor = if (isInDarkMode) Color.Black else Color.White
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
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> {
|
fun getAllAttributes(): List<Attribute> {
|
||||||
return listOf(body, agility, reaction, strength, willpower, logic, intuition, charisma)
|
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