This commit was merged in pull request #65.
This commit is contained in:
@@ -37,6 +37,20 @@ object TestTags {
|
|||||||
const val ATTRIBUTE_EDIT_DISMISS = "attribute_edit_dismiss"
|
const val ATTRIBUTE_EDIT_DISMISS = "attribute_edit_dismiss"
|
||||||
const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error"
|
const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error"
|
||||||
|
|
||||||
|
// --- Character data edit dialog ---
|
||||||
|
const val CHARACTER_DATA_EDIT_DIALOG = "character_data_edit_dialog"
|
||||||
|
const val CHARACTER_DATA_EDIT_NAME = "character_data_edit_name"
|
||||||
|
const val CHARACTER_DATA_EDIT_CONCEPT = "character_data_edit_concept"
|
||||||
|
const val CHARACTER_DATA_EDIT_GENDER = "character_data_edit_gender"
|
||||||
|
const val CHARACTER_DATA_EDIT_METATYPE = "character_data_edit_metatype"
|
||||||
|
const val CHARACTER_DATA_EDIT_AGE = "character_data_edit_age"
|
||||||
|
const val CHARACTER_DATA_EDIT_NUYEN = "character_data_edit_nuyen"
|
||||||
|
const val CHARACTER_DATA_EDIT_ESSENCE = "character_data_edit_essence"
|
||||||
|
const val CHARACTER_DATA_EDIT_TOTAL_KARMA = "character_data_edit_total_karma"
|
||||||
|
const val CHARACTER_DATA_EDIT_CURRENT_KARMA = "character_data_edit_current_karma"
|
||||||
|
const val CHARACTER_DATA_EDIT_CONFIRM = "character_data_edit_confirm"
|
||||||
|
const val CHARACTER_DATA_EDIT_DISMISS = "character_data_edit_dismiss"
|
||||||
|
|
||||||
// --- Talent edit dialog ---
|
// --- Talent edit dialog ---
|
||||||
const val TALENT_EDIT_DIALOG = "talent_edit_dialog"
|
const val TALENT_EDIT_DIALOG = "talent_edit_dialog"
|
||||||
const val TALENT_EDIT_INPUT = "talent_edit_input"
|
const val TALENT_EDIT_INPUT = "talent_edit_input"
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package org.shahondin1624.lib.components.charactermodel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
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.characterdata.CharacterData
|
||||||
|
import org.shahondin1624.model.characterdata.Metatype
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for editing character data fields: name, concept, metatype,
|
||||||
|
* age, gender, nuyen, essence, karma (total and current).
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CharacterDataEditDialog(
|
||||||
|
characterData: CharacterData,
|
||||||
|
onConfirm: (CharacterData) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf(characterData.name) }
|
||||||
|
var concept by remember { mutableStateOf(characterData.concept) }
|
||||||
|
var gender by remember { mutableStateOf(characterData.gender) }
|
||||||
|
var metatype by remember { mutableStateOf(characterData.metatype) }
|
||||||
|
var ageText by remember { mutableStateOf(characterData.age.toString()) }
|
||||||
|
var nuyenText by remember { mutableStateOf(characterData.nuyen.toString()) }
|
||||||
|
var essenceText by remember { mutableStateOf(formatEssenceForEdit(characterData.essence)) }
|
||||||
|
var totalKarmaText by remember { mutableStateOf(characterData.totalKarma.toString()) }
|
||||||
|
var currentKarmaText by remember { mutableStateOf(characterData.currentKarma.toString()) }
|
||||||
|
|
||||||
|
var metatypeExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val age = ageText.toIntOrNull()
|
||||||
|
val nuyen = nuyenText.toIntOrNull()
|
||||||
|
val essence = essenceText.toFloatOrNull()
|
||||||
|
val totalKarma = totalKarmaText.toIntOrNull()
|
||||||
|
val currentKarma = currentKarmaText.toIntOrNull()
|
||||||
|
|
||||||
|
val isValid = name.isNotBlank() &&
|
||||||
|
age != null && age >= 0 &&
|
||||||
|
nuyen != null && nuyen >= 0 &&
|
||||||
|
essence != null && essence in 0.0f..6.0f &&
|
||||||
|
totalKarma != null && totalKarma >= 0 &&
|
||||||
|
currentKarma != null && currentKarma >= 0 &&
|
||||||
|
currentKarma <= totalKarma
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_DIALOG),
|
||||||
|
title = { Text("Edit Character") },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// Name
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = { Text("Name") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_NAME)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Concept
|
||||||
|
OutlinedTextField(
|
||||||
|
value = concept,
|
||||||
|
onValueChange = { concept = it },
|
||||||
|
label = { Text("Concept") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_CONCEPT)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gender
|
||||||
|
OutlinedTextField(
|
||||||
|
value = gender,
|
||||||
|
onValueChange = { gender = it },
|
||||||
|
label = { Text("Gender") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_GENDER)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metatype dropdown
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = metatypeExpanded,
|
||||||
|
onExpandedChange = { metatypeExpanded = it },
|
||||||
|
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_METATYPE)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = metatype.name,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Metatype") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = metatypeExpanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = metatypeExpanded,
|
||||||
|
onDismissRequest = { metatypeExpanded = false }
|
||||||
|
) {
|
||||||
|
Metatype.entries.forEach { mt ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(mt.name) },
|
||||||
|
onClick = {
|
||||||
|
metatype = mt
|
||||||
|
metatypeExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Age
|
||||||
|
OutlinedTextField(
|
||||||
|
value = ageText,
|
||||||
|
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) ageText = it },
|
||||||
|
label = { Text("Age") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
isError = age == null && ageText.isNotEmpty(),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_AGE)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nuyen
|
||||||
|
OutlinedTextField(
|
||||||
|
value = nuyenText,
|
||||||
|
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) nuyenText = it },
|
||||||
|
label = { Text("Nuyen") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
isError = nuyen == null && nuyenText.isNotEmpty(),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_NUYEN)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Essence
|
||||||
|
OutlinedTextField(
|
||||||
|
value = essenceText,
|
||||||
|
onValueChange = { newVal ->
|
||||||
|
if (newVal.isEmpty() || newVal.matches(Regex("^\\d*\\.?\\d*$"))) {
|
||||||
|
essenceText = newVal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text("Essence (0.0 - 6.0)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
isError = (essence == null || essence !in 0.0f..6.0f) && essenceText.isNotEmpty(),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_ESSENCE)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Total Karma
|
||||||
|
OutlinedTextField(
|
||||||
|
value = totalKarmaText,
|
||||||
|
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) totalKarmaText = it },
|
||||||
|
label = { Text("Total Karma") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_TOTAL_KARMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Current Karma
|
||||||
|
OutlinedTextField(
|
||||||
|
value = currentKarmaText,
|
||||||
|
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) currentKarmaText = it },
|
||||||
|
label = { Text("Current Karma") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
isError = currentKarma != null && totalKarma != null && currentKarma > totalKarma,
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.CHARACTER_DATA_EDIT_CURRENT_KARMA)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (isValid) {
|
||||||
|
onConfirm(
|
||||||
|
characterData.copy(
|
||||||
|
name = name,
|
||||||
|
concept = concept,
|
||||||
|
gender = gender,
|
||||||
|
metatype = metatype,
|
||||||
|
age = age!!,
|
||||||
|
nuyen = nuyen!!,
|
||||||
|
essence = essence!!,
|
||||||
|
totalKarma = totalKarma!!,
|
||||||
|
currentKarma = currentKarma!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isValid,
|
||||||
|
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_CONFIRM)
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_DISMISS)
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format essence float for editing, showing one decimal place.
|
||||||
|
*/
|
||||||
|
private fun formatEssenceForEdit(essence: Float): String {
|
||||||
|
val wholePart = essence.toInt()
|
||||||
|
val decimalPart = ((essence - wholePart) * 10 + 0.5f).toInt()
|
||||||
|
return "$wholePart.$decimalPart"
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.shahondin1624.lib.components.charactermodel
|
package org.shahondin1624.lib.components.charactermodel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -19,7 +20,10 @@ import org.shahondin1624.theme.WindowSizeClass
|
|||||||
* Stacks vertically on Compact, horizontal row on Expanded.
|
* Stacks vertically on Compact, horizontal row on Expanded.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CharacterHeader(characterData: CharacterData) {
|
fun CharacterHeader(
|
||||||
|
characterData: CharacterData,
|
||||||
|
onEdit: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val spacing = UiConstants.Spacing.medium(windowSizeClass)
|
val spacing = UiConstants.Spacing.medium(windowSizeClass)
|
||||||
|
|
||||||
@@ -27,6 +31,10 @@ fun CharacterHeader(characterData: CharacterData) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag(TestTags.PANEL_CHARACTER_HEADER)
|
.testTag(TestTags.PANEL_CHARACTER_HEADER)
|
||||||
|
.then(
|
||||||
|
if (onEdit != null) Modifier.clickable { onEdit() }
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
when (windowSizeClass) {
|
when (windowSizeClass) {
|
||||||
WindowSizeClass.Compact -> CompactHeader(characterData, spacing)
|
WindowSizeClass.Compact -> CompactHeader(characterData, spacing)
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ fun CharacterSheetPage(
|
|||||||
// Talent edit dialog state
|
// Talent edit dialog state
|
||||||
var editingTalent by remember { mutableStateOf<TalentDefinition?>(null) }
|
var editingTalent by remember { mutableStateOf<TalentDefinition?>(null) }
|
||||||
|
|
||||||
|
// Character data edit dialog state
|
||||||
|
var editingCharacterData by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Show dice roll result dialog
|
// Show dice roll result dialog
|
||||||
pendingRoll?.let { roll ->
|
pendingRoll?.let { roll ->
|
||||||
DiceRollResultDialog(
|
DiceRollResultDialog(
|
||||||
@@ -99,6 +102,20 @@ fun CharacterSheetPage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show character data edit dialog
|
||||||
|
if (editingCharacterData) {
|
||||||
|
CharacterDataEditDialog(
|
||||||
|
characterData = character.characterData,
|
||||||
|
onConfirm = { newData ->
|
||||||
|
onUpdateCharacter { char ->
|
||||||
|
char.copy(characterData = newData)
|
||||||
|
}
|
||||||
|
editingCharacterData = false
|
||||||
|
},
|
||||||
|
onDismiss = { editingCharacterData = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Tab row
|
// Tab row
|
||||||
TabRow(
|
TabRow(
|
||||||
@@ -120,12 +137,15 @@ fun CharacterSheetPage(
|
|||||||
val onEditTalent: (TalentDefinition) -> Unit = { talent ->
|
val onEditTalent: (TalentDefinition) -> Unit = { talent ->
|
||||||
editingTalent = talent
|
editingTalent = talent
|
||||||
}
|
}
|
||||||
|
val onEditCharacterData: () -> Unit = {
|
||||||
|
editingCharacterData = true
|
||||||
|
}
|
||||||
|
|
||||||
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, onEditCharacterData)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
@@ -133,7 +153,7 @@ fun CharacterSheetPage(
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> OverviewContent(character, spacing)
|
CharacterTab.Overview -> OverviewContent(character, spacing, onEditCharacterData)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
@@ -144,7 +164,11 @@ fun CharacterSheetPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OverviewContent(character: ShadowrunCharacter, spacing: Dp) {
|
private fun OverviewContent(
|
||||||
|
character: ShadowrunCharacter,
|
||||||
|
spacing: Dp,
|
||||||
|
onEditCharacterData: () -> Unit
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -152,14 +176,18 @@ private fun OverviewContent(character: ShadowrunCharacter, spacing: Dp) {
|
|||||||
.padding(spacing),
|
.padding(spacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
verticalArrangement = Arrangement.spacedBy(spacing)
|
||||||
) {
|
) {
|
||||||
CharacterHeader(character.characterData)
|
CharacterHeader(character.characterData, onEdit = onEditCharacterData)
|
||||||
ResourcePanel(character.characterData, character.attributes.edge)
|
ResourcePanel(character.characterData, character.attributes.edge)
|
||||||
DerivedAttributesPanel(character.attributes)
|
DerivedAttributesPanel(character.attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp) {
|
private fun ExpandedOverviewContent(
|
||||||
|
character: ShadowrunCharacter,
|
||||||
|
spacing: Dp,
|
||||||
|
onEditCharacterData: () -> Unit
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -167,7 +195,7 @@ private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp)
|
|||||||
.padding(spacing),
|
.padding(spacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
verticalArrangement = Arrangement.spacedBy(spacing)
|
||||||
) {
|
) {
|
||||||
CharacterHeader(character.characterData)
|
CharacterHeader(character.characterData, onEdit = onEditCharacterData)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||||
|
|||||||
Reference in New Issue
Block a user