From 4c3586522f4aae68c6e3e35dc3fbbfd0f9e1de21 Mon Sep 17 00:00:00 2001 From: Alexandre G Pereira Date: Thu, 25 Jan 2024 04:07:45 -0300 Subject: [PATCH] Implement action add new items on monster edit (#241) * Implement action add new items on monster edit * Implement add new items on monster edit --- .../hunter/data/IosDispatchers.kt | 6 +- .../hunter/domain/model/AbilityDescription.kt | 14 +- .../hunter/domain/model/Action.kt | 31 ++- .../hunter/domain/model/Condition.kt | 15 +- .../hunter/domain/model/Damage.kt | 15 +- .../hunter/domain/model/SavingThrow.kt | 16 +- .../hunter/domain/model/Skill.kt | 15 +- .../hunter/domain/model/SpeedValue.kt | 16 +- .../monster/spell/model/SpellPreview.kt | 17 +- .../domain/monster/spell/model/SpellUsage.kt | 14 +- .../monster/spell/model/Spellcasting.kt | 16 +- .../hunter/detail/ui/ActionBlock.kt | 2 +- .../hunter/detail/ui/MonsterInfo.kt | 22 +- .../ui/MonsterRegistrationForm.kt | 219 ++++++++---------- .../ui/MonsterRegistrationScreen.kt | 1 + .../monster/registration/ui/form/AddButton.kt | 45 ---- .../registration/ui/form/AddRemoveButton.kt | 130 +++++++++++ .../monster/registration/ui/form/FormItems.kt | 53 +++++ .../registration/ui/form/FormLazyItem.kt | 48 ++++ .../ui/form/MonsterAbilityDescriptionForm.kt | 74 +++--- .../ui/form/MonsterAbilityScoresForm.kt | 36 +-- .../ui/form/MonsterActionsForm.kt | 129 +++++++---- .../ui/form/MonsterConditionsForm.kt | 60 ++--- .../ui/form/MonsterDamagesForm.kt | 81 ++++--- .../registration/ui/form/MonsterHeaderForm.kt | 114 ++++----- .../ui/form/MonsterProficiencyForm.kt | 70 +++--- .../ui/form/MonsterSavingThrowsForm.kt | 85 +++---- .../ui/form/MonsterSpeedValuesForm.kt | 98 ++++---- .../ui/form/MonsterSpellcastingsForm.kt | 148 +++++++----- .../registration/ui/form/MonsterStatsForm.kt | 88 +++---- .../ui/form/MonsterStringValueForm.kt | 37 ++- .../android/src/main/res/drawable/ic_add.xml | 5 - .../src/main/res/values-pt-rBR/strings.xml | 17 +- .../android/src/main/res/values/strings.xml | 17 +- .../registration/MonsterRegistrationState.kt | 1 + .../MonsterRegistrationStateHolder.kt | 30 ++- .../domain/NormalizeMonsterUseCase.kt | 89 ++++++- 37 files changed, 1199 insertions(+), 675 deletions(-) delete mode 100644 feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddButton.kt create mode 100644 feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddRemoveButton.kt create mode 100644 feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormItems.kt create mode 100644 feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormLazyItem.kt delete mode 100644 feature/monster-registration/android/src/main/res/drawable/ic_add.xml diff --git a/domain/app/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/IosDispatchers.kt b/domain/app/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/IosDispatchers.kt index 958701ee..07ab6585 100644 --- a/domain/app/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/IosDispatchers.kt +++ b/domain/app/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/IosDispatchers.kt @@ -16,6 +16,8 @@ package br.alexandregpereira.hunter.data +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.StableRef import kotlinx.cinterop.autoreleasepool import kotlinx.cinterop.cstr @@ -34,8 +36,9 @@ object IosDispatchers { val IO: CoroutineDispatcher = NSQueueDispatcher(QOS_CLASS_UTILITY) } +@OptIn(ExperimentalForeignApi::class) class NSQueueDispatcher(private val qosClass: qos_class_t) : CoroutineDispatcher() { - private val queue = dispatch_get_global_queue(qosClass.toLong(), 0) + private val queue = dispatch_get_global_queue(qosClass.toLong(), 0.toULong()) init { val key = "NSQueueDispatcher:$qosClass" @@ -45,6 +48,7 @@ class NSQueueDispatcher(private val qosClass: qos_class_t) : CoroutineDispatcher } } + @OptIn(BetaInteropApi::class) override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(queue) { autoreleasepool { diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityDescription.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityDescription.kt index 15947ffe..7f7a4ba7 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityDescription.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityDescription.kt @@ -22,4 +22,16 @@ import kotlin.native.ObjCName data class AbilityDescription( val name: String, val description: String -) +) { + + companion object { + + fun create( + name: String = "", + description: String = "" + ) = AbilityDescription( + name = name, + description = description + ) + } +} diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Action.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Action.kt index c38c0731..56590c41 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Action.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Action.kt @@ -24,10 +24,37 @@ data class Action( val damageDices: List, val attackBonus: Int?, val abilityDescription: AbilityDescription -) +) { + + companion object { + + fun create( + damageDices: List = emptyList(), + attackBonus: Int? = null, + abilityDescription: AbilityDescription = AbilityDescription.create() + ) = Action( + id = "", + damageDices = damageDices, + attackBonus = attackBonus, + abilityDescription = abilityDescription + ) + } +} @ObjCName(name = "DamageDice", exact = true) data class DamageDice( val dice: String, val damage: Damage -) +) { + + companion object { + + fun create( + dice: String = "", + damage: Damage = Damage.create() + ) = DamageDice( + dice = dice, + damage = damage + ) + } +} diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Condition.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Condition.kt index 529282d8..98a700ed 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Condition.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Condition.kt @@ -23,7 +23,20 @@ data class Condition( val index: String, val type: ConditionType, val name: String -) +) { + + companion object { + + fun create( + type: ConditionType = ConditionType.BLINDED, + name: String = "" + ) = Condition( + index = "", + type = type, + name = name + ) + } +} @ObjCName(name = "ConditionType", exact = true) enum class ConditionType { diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Damage.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Damage.kt index a267beb0..301b58f0 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Damage.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Damage.kt @@ -23,7 +23,20 @@ data class Damage( val index: String, val type: DamageType, val name: String -) +) { + + companion object { + + fun create( + type: DamageType = DamageType.ACID, + name: String = "" + ) = Damage( + index = "", + type = type, + name = name + ) + } +} @ObjCName(name = "DamageType", exact = true) enum class DamageType { diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SavingThrow.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SavingThrow.kt index e2abb4c8..bd1ae081 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SavingThrow.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SavingThrow.kt @@ -23,4 +23,18 @@ data class SavingThrow( val index: String, val modifier: Int, val type: AbilityScoreType -) +) { + companion object { + + fun create( + modifier: Int = 0, + type: AbilityScoreType = AbilityScoreType.STRENGTH, + ): SavingThrow { + return SavingThrow( + index = "", + modifier = modifier, + type = type + ) + } + } +} diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Skill.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Skill.kt index 97abde50..d0a2a752 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Skill.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Skill.kt @@ -23,4 +23,17 @@ data class Skill( val index: String, val modifier: Int, val name: String -) +) { + + companion object { + + fun create( + modifier: Int = 0, + name: String = "", + ) = Skill( + index = "", + modifier = modifier, + name = name + ) + } +} diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SpeedValue.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SpeedValue.kt index e22d43ca..ce7e03e5 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SpeedValue.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/SpeedValue.kt @@ -28,7 +28,21 @@ data class Speed( data class SpeedValue( val type: SpeedType, val valueFormatted: String -) +) { + + companion object { + + fun create( + type: SpeedType = SpeedType.WALK, + valueFormatted: String = "", + ): SpeedValue { + return SpeedValue( + type = type, + valueFormatted = valueFormatted + ) + } + } +} @ObjCName(name = "SpeedType", exact = true) enum class SpeedType { diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellPreview.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellPreview.kt index 982b5123..fdcbd982 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellPreview.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellPreview.kt @@ -24,7 +24,22 @@ data class SpellPreview( val name: String, val level: Int, val school: SchoolOfMagic, -) +) { + + companion object { + + fun create( + name: String = "", + level: Int = 0, + school: SchoolOfMagic = SchoolOfMagic.ABJURATION + ) = SpellPreview( + index = "", + name = name, + level = level, + school = school, + ) + } +} @ObjCName(name = "SchoolOfMagic", exact = true) enum class SchoolOfMagic { diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellUsage.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellUsage.kt index 4bed288c..422e0e52 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellUsage.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellUsage.kt @@ -22,4 +22,16 @@ import kotlin.native.ObjCName data class SpellUsage( val group: String, val spells: List -) +) { + + companion object { + + fun create( + group: String = "", + spells: List = emptyList() + ) = SpellUsage( + group = group, + spells = spells + ) + } +} diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/Spellcasting.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/Spellcasting.kt index 3e93a3ed..cffb9513 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/Spellcasting.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/Spellcasting.kt @@ -23,4 +23,18 @@ data class Spellcasting( val description: String, val type: SpellcastingType, val usages: List -) +) { + + companion object { + + fun create( + description: String = "", + type: SpellcastingType = SpellcastingType.SPELLCASTER, + usages: List = emptyList() + ) = Spellcasting( + description = description, + type = type, + usages = usages + ) + } +} diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/ActionBlock.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/ActionBlock.kt index 911c81c6..0890c4f1 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/ActionBlock.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/ActionBlock.kt @@ -54,7 +54,7 @@ private fun ActionDamageGrid( ) = Grid(modifier = modifier) { val iconSize = 48.dp - attackBonus?.let { + attackBonus.takeUnless { it == 0 }?.let { Bonus(value = it, name = stringResource(R.string.monster_detail_attack), iconSize = iconSize) } diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterInfo.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterInfo.kt index 7d2c6906..716eb678 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterInfo.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterInfo.kt @@ -86,17 +86,6 @@ fun LazyListScope.monsterInfo( onSpellClicked = onSpellClicked ) - item(key = "reactions") { - MonsterOptionalSectionAlphaTransition( - valueToValidate = { it.reactions }, - dataList = monsters, - pagerState = pagerState, - getItemsKeys = getItemsKeys, - ) { - ReactionBlock(reactions = it) - } - } - item(key = "space") { Spacer( modifier = Modifier @@ -297,6 +286,17 @@ private fun LazyListScope.monsterInfoPart5( } } + item(key = "reactions") { + MonsterOptionalSectionAlphaTransition( + valueToValidate = { it.reactions }, + dataList = monsters, + pagerState = pagerState, + getItemsKeys = getItemsKeys, + ) { + ReactionBlock(reactions = it) + } + } + item(key = "legendaryActions") { MonsterOptionalSectionAlphaTransition( valueToValidate = { it.legendaryActions }, diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationForm.kt index 7b4a10ab..f5c6e21e 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationForm.kt @@ -1,6 +1,5 @@ package br.alexandregpereira.hunter.monster.registration.ui -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -33,6 +32,7 @@ import br.alexandregpereira.hunter.ui.compose.AppButtonSize @Composable internal fun MonsterRegistrationForm( monster: Monster, + isSaveButtonEnabled: Boolean, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), intent: MonsterRegistrationIntent = EmptyMonsterRegistrationIntent(), @@ -40,7 +40,6 @@ internal fun MonsterRegistrationForm( modifier = modifier.fillMaxSize() ) { LazyColumn( - verticalArrangement = Arrangement.spacedBy(48.dp, Alignment.Top), modifier = Modifier.padding(bottom = contentPadding.calculateBottomPadding() + 32.dp + AppButtonSize.MEDIUM.height.dp), contentPadding = PaddingValues( start = 16.dp, @@ -49,124 +48,104 @@ internal fun MonsterRegistrationForm( bottom = contentPadding.calculateBottomPadding() + 16.dp, ), ) { - item(key = "monster") { - MonsterHeaderForm( - monster = monster, - onMonsterChanged = intent::onMonsterChanged, - ) - } - item(key = "stats") { - MonsterStatsForm( - monster = monster, - onMonsterChanged = intent::onMonsterChanged, - ) - } - item(key = "speed") { - MonsterSpeedValuesForm( - monster = monster, - onMonsterChanged = intent::onMonsterChanged, - ) - } - item(key = "abilityScores") { - MonsterAbilityScoresForm( - abilityScores = monster.abilityScores, - onChanged = { intent.onMonsterChanged(monster.copy(abilityScores = it)) } - ) - } - item(key = "savingThrows") { - MonsterSavingThrowsForm( - savingThrows = monster.savingThrows, - onChanged = { intent.onMonsterChanged(monster.copy(savingThrows = it)) }, - ) - } - item(key = "skills") { - MonsterProficiencyForm( - title = stringResource(R.string.monster_registration_skills), - proficiencies = monster.skills, - onChanged = { intent.onMonsterChanged(monster.copy(skills = it)) }, - ) - } - item(key = "damageVulnerabilities") { - MonsterDamagesForm( - title = stringResource(R.string.monster_registration_damage_vulnerabilities), - damages = monster.damageVulnerabilities, - onChanged = { intent.onMonsterChanged(monster.copy(damageVulnerabilities = it)) }, - ) - } - item(key = "damageResistances") { - MonsterDamagesForm( - title = stringResource(R.string.monster_registration_damage_resistances), - damages = monster.damageResistances, - onChanged = { intent.onMonsterChanged(monster.copy(damageResistances = it)) }, - ) - } - item(key = "damageImmunities") { - MonsterDamagesForm( - title = stringResource(R.string.monster_registration_damage_immunities), - damages = monster.damageImmunities, - onChanged = { intent.onMonsterChanged(monster.copy(damageImmunities = it)) }, - ) - } - item(key = "conditionImmunities") { - MonsterConditionsForm( - title = stringResource(R.string.monster_registration_condition_immunities), - conditions = monster.conditionImmunities, - onChanged = { intent.onMonsterChanged(monster.copy(conditionImmunities = it)) }, - ) - } - item(key = "senses") { - MonsterStringValueForm( - title = stringResource(R.string.monster_registration_senses), - value = monster.senses.joinToString(", "), - onChanged = { intent.onMonsterChanged(monster.copy(senses = it.split(", "))) }, - ) - } - item(key = "languages") { - MonsterStringValueForm( - title = stringResource(R.string.monster_registration_languages), - value = monster.languages, - onChanged = { intent.onMonsterChanged(monster.copy(languages = it)) }, - ) - } - item(key = "specialAbilities") { - MonsterAbilityDescriptionForm( - title = stringResource(R.string.monster_registration_special_abilities), - abilityDescriptions = monster.specialAbilities, - onChanged = { intent.onMonsterChanged(monster.copy(specialAbilities = it)) }, - ) - } - item(key = "actions") { - MonsterActionsForm( - title = stringResource(R.string.monster_registration_actions), - actions = monster.actions, - onChanged = { intent.onMonsterChanged(monster.copy(actions = it)) }, - ) - } - item(key = "reactions") { - MonsterAbilityDescriptionForm( - title = stringResource(R.string.monster_registration_reactions), - abilityDescriptions = monster.reactions, - onChanged = { intent.onMonsterChanged(monster.copy(reactions = it)) }, - ) - } - item(key = "legendaryActions") { - MonsterActionsForm( - title = stringResource(R.string.monster_registration_legendary_actions), - actions = monster.legendaryActions, - onChanged = { intent.onMonsterChanged(monster.copy(legendaryActions = it)) }, - ) - } - item(key = "spells") { - MonsterSpellcastingsForm( - spellcastings = monster.spellcastings, - onChanged = { intent.onMonsterChanged(monster.copy(spellcastings = it)) }, - onSpellClick = intent::onSpellClick, - ) - } + MonsterHeaderForm( + monster = monster, + onMonsterChanged = intent::onMonsterChanged, + ) + MonsterStatsForm( + stats = monster.stats, + onChanged = { intent.onMonsterChanged(monster.copy(stats = it)) } + ) + MonsterSpeedValuesForm( + monster = monster, + onMonsterChanged = intent::onMonsterChanged, + ) + MonsterAbilityScoresForm( + abilityScores = monster.abilityScores, + onChanged = { intent.onMonsterChanged(monster.copy(abilityScores = it)) } + ) + MonsterSavingThrowsForm( + savingThrows = monster.savingThrows, + onChanged = { intent.onMonsterChanged(monster.copy(savingThrows = it)) }, + ) + MonsterProficiencyForm( + title = { stringResource(R.string.monster_registration_skills) }, + proficiencies = monster.skills, + onChanged = { intent.onMonsterChanged(monster.copy(skills = it)) }, + ) + MonsterDamagesForm( + key = "damageVulnerabilities", + title = { stringResource(R.string.monster_registration_damage_vulnerabilities) }, + damages = monster.damageVulnerabilities, + onChanged = { intent.onMonsterChanged(monster.copy(damageVulnerabilities = it)) }, + ) + MonsterDamagesForm( + key = "damageResistances", + title = { stringResource(R.string.monster_registration_damage_resistances) }, + damages = monster.damageResistances, + onChanged = { intent.onMonsterChanged(monster.copy(damageResistances = it)) }, + ) + MonsterDamagesForm( + key = "damageImmunities", + title = { stringResource(R.string.monster_registration_damage_immunities) }, + damages = monster.damageImmunities, + onChanged = { intent.onMonsterChanged(monster.copy(damageImmunities = it)) }, + ) + MonsterConditionsForm( + title = { stringResource(R.string.monster_registration_condition_immunities) }, + conditions = monster.conditionImmunities, + onChanged = { intent.onMonsterChanged(monster.copy(conditionImmunities = it)) }, + ) + MonsterStringValueForm( + key = "senses", + title = { stringResource(R.string.monster_registration_senses) }, + value = monster.senses.joinToString(", "), + onChanged = { intent.onMonsterChanged(monster.copy(senses = it.split(", "))) }, + ) + MonsterStringValueForm( + key = "languages", + title = { stringResource(R.string.monster_registration_languages) }, + value = monster.languages, + onChanged = { intent.onMonsterChanged(monster.copy(languages = it)) }, + ) + MonsterAbilityDescriptionForm( + key = "specialAbilities", + title = { stringResource(R.string.monster_registration_special_abilities) }, + abilityDescriptions = monster.specialAbilities, + addText = { stringResource(R.string.monster_registration_add_special_ability) }, + removeText = { stringResource(R.string.monster_registration_remove_special_ability) }, + onChanged = { intent.onMonsterChanged(monster.copy(specialAbilities = it)) }, + ) + MonsterActionsForm( + key = "actions", + title = { stringResource(R.string.monster_registration_actions) }, + actions = monster.actions, + onChanged = { intent.onMonsterChanged(monster.copy(actions = it)) }, + ) + MonsterAbilityDescriptionForm( + key = "reactions", + title = { stringResource(R.string.monster_registration_reactions) }, + abilityDescriptions = monster.reactions, + addText = { stringResource(R.string.monster_registration_add_reaction) }, + removeText = { stringResource(R.string.monster_registration_remove_reaction) }, + onChanged = { intent.onMonsterChanged(monster.copy(reactions = it)) }, + ) + MonsterActionsForm( + key = "legendaryActions", + title = { stringResource(R.string.monster_registration_legendary_actions) }, + actions = monster.legendaryActions, + onChanged = { intent.onMonsterChanged(monster.copy(legendaryActions = it)) }, + ) + MonsterSpellcastingsForm( + spellcastings = monster.spellcastings, + onChanged = { intent.onMonsterChanged(monster.copy(spellcastings = it)) }, + onSpellClick = intent::onSpellClick, + ) } AppButton( text = "Save", + enabled = isSaveButtonEnabled, modifier = Modifier .padding(horizontal = 16.dp) .padding(bottom = contentPadding.calculateBottomPadding() + 16.dp) @@ -183,3 +162,11 @@ internal fun MutableList.changeAt( it[index] = it[index].copy() } } + +internal fun MutableList.alsoAdd(index: Int, value: T): List { + return also { it.add(index, value) } +} + +internal fun MutableList.alsoRemoveAt(index: Int): List { + return also { it.removeAt(index) } +} diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt index a159a180..7ccc762b 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt @@ -30,6 +30,7 @@ internal fun MonsterRegistrationScreen( ) { MonsterRegistrationForm( monster = state.monster, + isSaveButtonEnabled = state.isSaveButtonEnabled, modifier = Modifier, contentPadding = contentPadding, intent = intent, diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddButton.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddButton.kt deleted file mode 100644 index 0bf6e854..00000000 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddButton.kt +++ /dev/null @@ -1,45 +0,0 @@ -package br.alexandregpereira.hunter.monster.registration.ui.form - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import br.alexandregpereira.hunter.monster.registration.R - -@Composable -internal fun AddButton( - modifier: Modifier = Modifier, - text: String = "", - onClick: () -> Unit = {}, -) = Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .height(40.dp) - .fillMaxWidth() - .clickable { onClick() }, -) { - val string = text.ifEmpty { - stringResource(R.string.monster_registration_add_new) - } - Icon( - painter = painterResource(R.drawable.ic_add), - contentDescription = string, - modifier = Modifier.padding(end = 8.dp), - ) - Text( - text = string, - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - ) -} diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddRemoveButton.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddRemoveButton.kt new file mode 100644 index 00000000..2833de0e --- /dev/null +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/AddRemoveButton.kt @@ -0,0 +1,130 @@ +package br.alexandregpereira.hunter.monster.registration.ui.form + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.alexandregpereira.hunter.monster.registration.R +import br.alexandregpereira.hunter.ui.compose.Window + +@Composable +internal fun AddRemoveButtons( + modifier: Modifier = Modifier, + addText: String = stringResource(R.string.monster_registration_add_new), + removeText: String = "", + onAdd: () -> Unit = {}, + onRemove: () -> Unit = {}, +) = Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, +) { + AddButton( + text = addText, + modifier = Modifier.weight(1f), + onClick = onAdd, + ) + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier.weight(1f) + ) { + AnimatedRemoveButton( + removeText = removeText, + onRemove = onRemove, + ) + } +} + +@Composable +private fun AnimatedRemoveButton( + removeText: String, + onRemove: () -> Unit = {}, +) = AnimatedVisibility( + visible = removeText.isNotBlank(), + enter = fadeIn(), + exit = fadeOut(), +) { + RemoveButton( + text = removeText, + onClick = onRemove, + ) +} + +@Composable +internal fun AddButton( + modifier: Modifier = Modifier, + text: String = stringResource(R.string.monster_registration_add_new), + onClick: () -> Unit = {}, +) = ItemButton( + icon = Icons.Default.Add, + modifier = modifier, + text = text, + onClick = onClick, +) + +@Composable +internal fun RemoveButton( + modifier: Modifier = Modifier, + text: String = "", + onClick: () -> Unit = {}, +) = ItemButton( + icon = Icons.Default.Delete, + modifier = modifier, + text = text, + onClick = onClick, +) + +@Composable +private fun ItemButton( + icon: ImageVector, + text: String, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) = Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable { onClick() }, +) { + Icon( + imageVector = icon, + contentDescription = text, + modifier = Modifier + .padding(end = 8.dp) + .padding(vertical = 16.dp), + ) + Text( + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(vertical = 16.dp), + ) +} + +@Preview +@Composable +private fun AddRemoveButtonsPreview() = Window { + AddRemoveButtons( + addText = "Add special ability", + removeText = "Remove ability", + ) +} diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormItems.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormItems.kt new file mode 100644 index 00000000..b6e75c4c --- /dev/null +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormItems.kt @@ -0,0 +1,53 @@ +package br.alexandregpereira.hunter.monster.registration.ui.form + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import br.alexandregpereira.hunter.monster.registration.R +import br.alexandregpereira.hunter.monster.registration.ui.alsoAdd +import br.alexandregpereira.hunter.monster.registration.ui.alsoRemoveAt + +@Suppress("FunctionName") +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.FormItems( + items: MutableList, + addText: @Composable () -> String = { stringResource(R.string.monster_registration_add) }, + removeText: @Composable () -> String = { stringResource(R.string.monster_registration_remove) }, + key: String, + createNew: () -> T, + onChanged: (List) -> Unit = {}, + content: LazyListScope.(Int, T) -> Unit +) { + formItem(key = "$key-add-remove-buttons") { + AddRemoveButtons( + addText = addText(), + removeText = removeText().takeUnless { items.isEmpty() }.orEmpty(), + onAdd = { + onChanged(items.alsoAdd(0, createNew())) + }, + onRemove = { + onChanged(items.alsoRemoveAt(0)) + }, + ) + } + + items.forEachIndexed { index, item -> + content(index, item) + + formItem(key = "$key-add-remove-buttons-$index") { + AddRemoveButtons( + addText = addText(), + removeText = removeText().takeUnless { index == items.lastIndex }.orEmpty(), + onAdd = { + onChanged(items.alsoAdd(index + 1, createNew())) + }, + onRemove = { + onChanged(items.alsoRemoveAt(index + 1)) + }, + modifier = Modifier.animateItemPlacement(), + ) + } + } +} diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormLazyItem.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormLazyItem.kt new file mode 100644 index 00000000..47a0b07c --- /dev/null +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/FormLazyItem.kt @@ -0,0 +1,48 @@ +package br.alexandregpereira.hunter.monster.registration.ui.form + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import br.alexandregpereira.hunter.ui.compose.ScreenHeader + +@Suppress("FunctionName") +internal fun LazyListScope.FormLazy( + key: String, + title: @Composable () -> String, + modifier: Modifier = Modifier, + content: LazyListScope.() -> Unit, +) { + formItem(key = "$key-title") { + Column(modifier) { + Spacer(modifier = Modifier.height(16.dp)) + ScreenHeader( + title = title(), + ) + } + } + + content() +} + +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.formItem( + key: String, + modifier: Modifier = Modifier, + content: @Composable LazyItemScope.() -> Unit +) = item(key) { + Box( + modifier = modifier + .padding(vertical = 8.dp) + .animateItemPlacement() + ) { + content() + } +} diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityDescriptionForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityDescriptionForm.kt index 8a5efd86..7a1e0230 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityDescriptionForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityDescriptionForm.kt @@ -1,54 +1,50 @@ package br.alexandregpereira.hunter.monster.registration.ui.form -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material.Text +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import br.alexandregpereira.hunter.domain.model.AbilityDescription import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form -@Composable -internal fun MonsterAbilityDescriptionForm( - title: String, +@Suppress("FunctionName") +internal fun LazyListScope.MonsterAbilityDescriptionForm( + key: String, + title: @Composable () -> String, abilityDescriptions: List, - modifier: Modifier = Modifier, + addText: @Composable () -> String, + removeText: @Composable () -> String, onChanged: (List) -> Unit = {}, - content: @Composable (Int) -> Unit = { }, -) = Form(modifier, title) { +) = FormLazy(key, title) { val newAbilityDescriptions = abilityDescriptions.toMutableList() - abilityDescriptions.forEachIndexed { index, abilityDescription -> - AppTextField( - text = abilityDescription.name, - label = stringResource(R.string.monster_registration_name), - onValueChange = { newValue -> - onChanged(newAbilityDescriptions.changeAt(index) { copy(name = newValue) }) - } - ) + FormItems( + key = key, + items = newAbilityDescriptions, + addText = addText, + removeText = removeText, + createNew = { AbilityDescription.create() }, + onChanged = onChanged + ) { index, abilityDescription -> + formItem(key = "$key-ability-description-name-$index") { + AppTextField( + text = abilityDescription.name, + label = stringResource(R.string.monster_registration_name), + onValueChange = { newValue -> + onChanged(newAbilityDescriptions.changeAt(index) { copy(name = newValue) }) + } + ) + } - AppTextField( - text = abilityDescription.description, - label = stringResource(R.string.monster_registration_description), - multiline = true, - onValueChange = { newValue -> - onChanged(newAbilityDescriptions.changeAt(index) { copy(description = newValue) }) - } - ) - - content(index) - - AddButton() - } - - if (abilityDescriptions.isEmpty()) { - AddButton() + formItem(key = "$key-ability-description-description-$index") { + AppTextField( + text = abilityDescription.description, + label = stringResource(R.string.monster_registration_description), + multiline = true, + onValueChange = { newValue -> + onChanged(newAbilityDescriptions.changeAt(index) { copy(description = newValue) }) + } + ) + } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityScoresForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityScoresForm.kt index 8aa85f68..f4596da8 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityScoresForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterAbilityScoresForm.kt @@ -1,32 +1,34 @@ package br.alexandregpereira.hunter.monster.registration.ui.form -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastForEachIndexed import br.alexandregpereira.hunter.domain.model.AbilityScore import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form -@Composable -internal fun MonsterAbilityScoresForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterAbilityScoresForm( abilityScores: List, - modifier: Modifier = Modifier, onChanged: (List) -> Unit = {} -) = Form( - modifier = modifier, - title = stringResource(R.string.monster_registration_ability_scores), ) { - val newAbilityScores = abilityScores.toMutableList() - abilityScores.fastForEachIndexed { i, abilityScore -> - AppTextField( - value = abilityScore.value, - label = abilityScore.type.toState().getStringResource(), - onValueChange = { newValue -> - onChanged(newAbilityScores.changeAt(i) { copy(value = newValue) }) + val key = "abilityScores" + FormLazy( + key = key, + title = { stringResource(R.string.monster_registration_ability_scores) }, + ) { + val newAbilityScores = abilityScores.toMutableList() + abilityScores.fastForEachIndexed { i, abilityScore -> + formItem(key = "$key-$i") { + AppTextField( + value = abilityScore.value, + label = abilityScore.type.toState().getStringResource(), + onValueChange = { newValue -> + onChanged(newAbilityScores.changeAt(i) { copy(value = newValue) }) + } + ) } - ) + } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterActionsForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterActionsForm.kt index 2c5d448f..704bcc96 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterActionsForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterActionsForm.kt @@ -1,7 +1,7 @@ package br.alexandregpereira.hunter.monster.registration.ui.form +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.model.Action import br.alexandregpereira.hunter.domain.model.DamageDice @@ -11,67 +11,114 @@ import br.alexandregpereira.hunter.monster.registration.ui.changeAt import br.alexandregpereira.hunter.ui.compose.AppTextField import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterActionsForm( - title: String, +@Suppress("FunctionName") +internal fun LazyListScope.MonsterActionsForm( + key: String, + title: @Composable () -> String, actions: List, - modifier: Modifier = Modifier, onChanged: (List) -> Unit = {} -) { +) = FormLazy(key, title) { val newActions = actions.toMutableList() val damageTypes = DamageType.entries.filter { it != DamageType.OTHER } - val damageTypeOptions = damageTypes.map { it.toTypeState().getStringName() } - MonsterAbilityDescriptionForm( - title = title, - abilityDescriptions = actions.map { it.abilityDescription }, - modifier = modifier, - onChanged = { - onChanged( - actions.mapIndexed { index, action -> - action.copy( - abilityDescription = it[index], - ) - } - ) - } - ) { actionIndex -> - val action = actions[actionIndex] - action.attackBonus?.let { + + FormItems( + items = newActions, + addText = { stringResource(R.string.monster_registration_add_action) }, + removeText = { stringResource(R.string.monster_registration_remove_action) }, + key = key, + createNew = { Action.create() }, + onChanged = onChanged + ) { actionIndex, action -> + val abilityDescription = action.abilityDescription + formItem(key = "$key-action-name-$actionIndex") { AppTextField( - value = it, - label = stringResource(R.string.monster_registration_attack_bonus), + text = abilityDescription.name, + label = stringResource(R.string.monster_registration_name), onValueChange = { newValue -> - onChanged(newActions.changeAt(actionIndex) { copy(attackBonus = newValue) }) + onChanged( + newActions.changeAt(actionIndex) { + copy( + abilityDescription = actions[actionIndex].abilityDescription.copy( + name = newValue + ) + ) + } + ) } ) } - action.damageDices.takeIf { it.isNotEmpty() }?.forEachIndexed { index, damageDice -> - PickerField( - value = damageDice.damage.type.toTypeState().getStringName(), - label = stringResource(R.string.monster_registration_damage_type), - options = damageTypeOptions, - onValueChange = { optionIndex -> + formItem(key = "$key-action-description-$actionIndex") { + AppTextField( + text = abilityDescription.description, + label = stringResource(R.string.monster_registration_description), + multiline = true, + onValueChange = { newValue -> onChanged( - newActions.changeDamageDiceAt(actionIndex, index) { - copy(damage = damage.copy(type = damageTypes[optionIndex])) + newActions.changeAt(actionIndex) { + copy( + abilityDescription = actions[actionIndex].abilityDescription.copy( + description = newValue + ) + ) } ) } ) + } + formItem(key = "$key-action-attackBonus-$actionIndex") { AppTextField( - text = damageDice.dice, - label = stringResource(R.string.monster_registration_damage_dice), + value = action.attackBonus ?: 0, + label = stringResource(R.string.monster_registration_attack_bonus), onValueChange = { newValue -> - onChanged( - newActions.changeDamageDiceAt(actionIndex, index) { - copy(dice = newValue) - } - ) + onChanged(newActions.changeAt(actionIndex) { copy(attackBonus = newValue) }) } ) } + + val damageDiceKey = "$key-actions-damageDices-$actionIndex" + FormItems( + items = action.damageDices.toMutableList(), + addText = { stringResource(R.string.monster_registration_add_damage_dice) }, + removeText = { stringResource(R.string.monster_registration_remove_damage_dice) }, + key = damageDiceKey, + createNew = { DamageDice.create() }, + onChanged = { + onChanged( + newActions.changeAt(actionIndex) { copy(damageDices = it) } + ) + } + ) { index, damageDice -> + formItem(key = "$damageDiceKey-damageDice-type-$index") { + PickerField( + value = damageDice.damage.type.toTypeState().getStringName(), + label = stringResource(R.string.monster_registration_damage_type), + options = damageTypes.map { it.toTypeState().getStringName() }, + onValueChange = { optionIndex -> + onChanged( + newActions.changeDamageDiceAt(actionIndex, index) { + copy(damage = damage.copy(type = damageTypes[optionIndex])) + } + ) + } + ) + } + + formItem(key = "$damageDiceKey-damageDice-dice-$index") { + AppTextField( + text = damageDice.dice, + label = stringResource(R.string.monster_registration_damage_dice), + onValueChange = { newValue -> + onChanged( + newActions.changeDamageDiceAt(actionIndex, index) { + copy(dice = newValue) + } + ) + } + ) + } + } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterConditionsForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterConditionsForm.kt index 8707eaf8..5495a4c9 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterConditionsForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterConditionsForm.kt @@ -1,21 +1,19 @@ package br.alexandregpereira.hunter.monster.registration.ui.form import androidx.annotation.StringRes +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.model.Condition import br.alexandregpereira.hunter.domain.model.ConditionType import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt -import br.alexandregpereira.hunter.ui.compose.Form import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterConditionsForm( - title: String, +@Suppress("FunctionName") +internal fun LazyListScope.MonsterConditionsForm( + title: @Composable () -> String, conditions: List, - modifier: Modifier = Modifier, onChanged: (List) -> Unit = {} ) { val newConditions = conditions.toMutableList() @@ -23,31 +21,35 @@ internal fun MonsterConditionsForm( val conditionTypes = ConditionType.entries.map { it.toTypeState() }.filterNot { currentConditionTypes.contains(it) } - val conditionTypeOptions = conditionTypes.map { stringResource(it.stringRes) } - Form( - modifier = modifier, + val key = "conditionImmunities" + FormLazy( + key = key, title = title, ) { - conditions.forEachIndexed { i, condition -> - PickerField( - value = stringResource(condition.type.toTypeState().stringRes), - label = stringResource(R.string.monster_registration_condition_type), - options = conditionTypeOptions, - onValueChange = { optionIndex -> - onChanged( - newConditions.changeAt(i) { - copy( - type = ConditionType.valueOf(conditionTypes[optionIndex].name), - name = conditionTypeOptions[optionIndex] - ) - } - ) - } - ) - } - - if (conditions.size < ConditionType.entries.size) { - AddButton() + FormItems( + key = key, + items = newConditions, + createNew = { Condition.create() }, + onChanged = onChanged + ) { i, condition -> + formItem(key = "$key-name-$i") { + val conditionTypeOptions = conditionTypes.map { stringResource(it.stringRes) } + PickerField( + value = stringResource(condition.type.toTypeState().stringRes), + label = stringResource(R.string.monster_registration_condition_type), + options = conditionTypeOptions, + onValueChange = { optionIndex -> + onChanged( + newConditions.changeAt(i) { + copy( + type = ConditionType.valueOf(conditionTypes[optionIndex].name), + name = conditionTypeOptions[optionIndex] + ) + } + ) + } + ) + } } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterDamagesForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterDamagesForm.kt index 2a85a5bc..a179c4f5 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterDamagesForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterDamagesForm.kt @@ -2,6 +2,7 @@ package br.alexandregpereira.hunter.monster.registration.ui.form import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -11,14 +12,13 @@ import br.alexandregpereira.hunter.domain.model.DamageType import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterDamagesForm( - title: String, +@Suppress("FunctionName") +internal fun LazyListScope.MonsterDamagesForm( + key: String, + title: @Composable () -> String, damages: List, - modifier: Modifier = Modifier, onChanged: (List) -> Unit = {} ) { val newDamages = damages.toMutableList() @@ -26,46 +26,51 @@ internal fun MonsterDamagesForm( val damageTypes = DamageType.entries.map { it.toTypeState() }.filterNot { currentDamageTypes.contains(it) } - val damageTypeOptions = damageTypes.map { stringResource(it.stringRes) } - Form( - modifier = modifier, + FormLazy( + key = key, title = title, ) { - damages.forEachIndexed { i, damage -> - if (i != 0 && damage.type == DamageType.OTHER) Spacer(modifier = Modifier.height(8.dp)) - - PickerField( - value = stringResource(damage.type.toTypeState().stringRes), - label = stringResource(R.string.monster_registration_damage_type), - options = damageTypeOptions, - onValueChange = { optionIndex -> - onChanged( - newDamages.changeAt(i) { - copy( - type = DamageType.valueOf(damageTypes[optionIndex].name), - name = damageTypeOptions[optionIndex].takeIf { - damageTypes[optionIndex] != DamageTypeState.OTHER - }.orEmpty() - ) - } - ) - } - ) + FormItems( + key = key, + items = newDamages, + createNew = { Damage.create() }, + onChanged = onChanged + ) { i, damage -> + formItem(key = "$key-name-$i") { + val damageTypeOptions = damageTypes.map { stringResource(it.stringRes) } + if (i != 0 && damage.type == DamageType.OTHER) Spacer(modifier = Modifier.height(8.dp)) - if (damage.type == DamageType.OTHER) { - AppTextField( - text = damage.name, - label = stringResource(R.string.monster_registration_damage_type_other), - onValueChange = { newValue -> - onChanged(newDamages.changeAt(i) { copy(name = newValue) }) + PickerField( + value = stringResource(damage.type.toTypeState().stringRes), + label = stringResource(R.string.monster_registration_damage_type), + options = damageTypeOptions, + onValueChange = { optionIndex -> + onChanged( + newDamages.changeAt(i) { + copy( + type = DamageType.valueOf(damageTypes[optionIndex].name), + name = damageTypeOptions[optionIndex].takeIf { + damageTypes[optionIndex] != DamageTypeState.OTHER + }.orEmpty() + ) + } + ) } ) - Spacer(modifier = Modifier.height(8.dp)) } - } - if (damages.size < DamageType.entries.size) { - AddButton() + formItem(key = "$key-name-other-$i") { + if (damage.type == DamageType.OTHER) { + AppTextField( + text = damage.name, + label = stringResource(R.string.monster_registration_damage_type_other), + onValueChange = { newValue -> + onChanged(newDamages.changeAt(i) { copy(name = newValue) }) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterHeaderForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterHeaderForm.kt index 801b348a..4f74ca1d 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterHeaderForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterHeaderForm.kt @@ -1,73 +1,83 @@ package br.alexandregpereira.hunter.monster.registration.ui.form import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.model.Color import br.alexandregpereira.hunter.domain.model.Monster import br.alexandregpereira.hunter.domain.model.MonsterType import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterHeaderForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterHeaderForm( monster: Monster, - modifier: Modifier = Modifier, onMonsterChanged: (Monster) -> Unit = {} -) = Form(modifier = modifier, title = stringResource(R.string.monster_registration_edit)) { - AppTextField( - text = monster.name, - label = stringResource(R.string.monster_registration_name), - onValueChange = { onMonsterChanged(monster.copy(name = it)) } - ) - - AppTextField( - text = monster.subtitle, - label = stringResource(R.string.monster_registration_name), - onValueChange = { onMonsterChanged(monster.copy(subtitle = it)) } - ) - - AppTextField( - text = monster.group.orEmpty(), - label = stringResource(R.string.monster_registration_group), - onValueChange = { - onMonsterChanged(monster.copy(group = it.takeUnless { it.isBlank() })) +) { + val key = "monsterHeader" + FormLazy( + key = key, + title = { stringResource(R.string.monster_registration_edit) } + ) { + formItem(key = "$key-name") { + AppTextField( + text = monster.name, + label = stringResource(R.string.monster_registration_name), + onValueChange = { onMonsterChanged(monster.copy(name = it)) } + ) } - ) - - AppTextField( - text = monster.imageData.url, - label = stringResource(R.string.monster_registration_image_url), - onValueChange = { - onMonsterChanged(monster.copy(imageData = monster.imageData.copy(url = it))) + formItem(key = "$key-subtitle") { + AppTextField( + text = monster.subtitle, + label = stringResource(R.string.monster_registration_subtitle), + onValueChange = { onMonsterChanged(monster.copy(subtitle = it)) } + ) } - ) - - AppTextField( - text = monster.imageData.backgroundColor.light, - label = stringResource(R.string.monster_registration_image_background_color), - onValueChange = { - onMonsterChanged( - monster.copy(imageData = monster.imageData.copy( - backgroundColor = Color(light = it, dark = it) - )) + formItem(key = "$key-group") { + AppTextField( + text = monster.group.orEmpty(), + label = stringResource(R.string.monster_registration_group), + onValueChange = { + onMonsterChanged(monster.copy(group = it.takeUnless { it.isBlank() })) + } ) } - ) - - PickerField( - value = stringResource(monster.type.toMonsterTypeState().stringRes), - label = stringResource(R.string.monster_registration_type), - options = MonsterType.entries.map { - stringResource(it.toMonsterTypeState().stringRes) - }, - onValueChange = { i -> - onMonsterChanged(monster.copy(type = MonsterType.entries[i])) + formItem(key = "$key-imageUrl") { + AppTextField( + text = monster.imageData.url, + label = stringResource(R.string.monster_registration_image_url), + onValueChange = { + onMonsterChanged(monster.copy(imageData = monster.imageData.copy(url = it))) + } + ) + } + formItem(key = "$key-imageBackgroundColor") { + AppTextField( + text = monster.imageData.backgroundColor.light, + label = stringResource(R.string.monster_registration_image_background_color), + onValueChange = { + onMonsterChanged( + monster.copy(imageData = monster.imageData.copy( + backgroundColor = Color(light = it, dark = it) + )) + ) + } + ) } - ) + formItem(key = "$key-type") { + PickerField( + value = stringResource(monster.type.toMonsterTypeState().stringRes), + label = stringResource(R.string.monster_registration_type), + options = MonsterType.entries.map { + stringResource(it.toMonsterTypeState().stringRes) + }, + onValueChange = { i -> + onMonsterChanged(monster.copy(type = MonsterType.entries[i])) + } + ) + } + } } private fun MonsterType.toMonsterTypeState(): MonsterTypeState { diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterProficiencyForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterProficiencyForm.kt index 2bca94c2..961675d9 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterProficiencyForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterProficiencyForm.kt @@ -1,54 +1,54 @@ package br.alexandregpereira.hunter.monster.registration.ui.form +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.model.Skill import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt -import br.alexandregpereira.hunter.ui.compose.AppKeyboardType import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form -@Composable -internal fun MonsterProficiencyForm( - title: String, +@Suppress("FunctionName") +internal fun LazyListScope.MonsterProficiencyForm( + title: @Composable () -> String, proficiencies: List, - modifier: Modifier = Modifier, onChanged: (List) -> Unit = {} ) { val mutableProficiencies = proficiencies.toMutableList() - Form( - modifier = modifier, + val key = "skills" + FormLazy( + key = key, title = title, ) { - proficiencies.forEachIndexed { i, proficiency -> - AppTextField( - text = proficiency.name, - label = stringResource(R.string.monster_registration_name), - onValueChange = { newValue -> - onChanged(mutableProficiencies.changeAt(i) { copy(name = newValue) }) - } - ) + FormItems( + key = key, + items = mutableProficiencies, + createNew = { Skill.create() }, + onChanged = onChanged + ) { i, proficiency -> + formItem(key = "$key-name-$i") { + AppTextField( + text = proficiency.name, + label = stringResource(R.string.monster_registration_name), + onValueChange = { newValue -> + onChanged(mutableProficiencies.changeAt(i) { copy(name = newValue) }) + } + ) + } - AppTextField( - text = proficiency.modifier.toString(), - label = proficiency.name, - keyboardType = AppKeyboardType.NUMBER, - onValueChange = { newValue -> - onChanged( - mutableProficiencies.changeAt(i) { - copy(modifier = newValue.toIntOrNull() ?: 0) - } - ) - } - ) - - AddButton() - } - - if (proficiencies.isEmpty()) { - AddButton() + formItem(key = "$key-modifier-$i") { + AppTextField( + value = proficiency.modifier, + label = proficiency.name, + onValueChange = { newValue -> + onChanged( + mutableProficiencies.changeAt(i) { + copy(modifier = newValue) + } + ) + } + ) + } } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSavingThrowsForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSavingThrowsForm.kt index a584c35e..b84087d8 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSavingThrowsForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSavingThrowsForm.kt @@ -1,65 +1,68 @@ package br.alexandregpereira.hunter.monster.registration.ui.form +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.model.AbilityScoreType import br.alexandregpereira.hunter.domain.model.SavingThrow import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt -import br.alexandregpereira.hunter.ui.compose.AppKeyboardType import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterSavingThrowsForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterSavingThrowsForm( savingThrows: List, - modifier: Modifier = Modifier, onChanged: (List) -> Unit = {} ) { val types = savingThrows.map { it.type.name } val options = AbilityScoreType.entries.filterNot { types.contains(it.name) } - val optionsString = AbilityScoreTypeState.entries.filterNot { types.contains(it.name) } - .map { it.getStringResource() } val mutableSavingThrows = savingThrows.toMutableList() + val key = "saving-throws" - Form( - modifier = modifier, - title = stringResource(R.string.monster_registration_saving_throws), + FormLazy( + key = key, + title = { stringResource(R.string.monster_registration_saving_throws) }, ) { - savingThrows.forEachIndexed { i, savingThrow -> - val typeName = savingThrow.type.toState().getStringResource() - - PickerField( - value = typeName, - label = stringResource(R.string.monster_registration_name), - options = optionsString, - onValueChange = { optionIndex -> - onChanged( - mutableSavingThrows.changeAt(i) { - copy(type = options[optionIndex]) - } - ) - } - ) - AppTextField( - text = savingThrow.modifier.toString(), - label = typeName, - keyboardType = AppKeyboardType.NUMBER, - onValueChange = { newValue -> - onChanged( - mutableSavingThrows.changeAt(i) { - copy(modifier = newValue.toIntOrNull() ?: 0) - } - ) - } - ) - } + FormItems( + key = key, + items = mutableSavingThrows, + createNew = { SavingThrow.create() }, + onChanged = onChanged + ) { i, savingThrow -> + formItem(key = "$key-name-$i") { + val typeName = savingThrow.type.toState().getStringResource() + val optionsString = AbilityScoreTypeState.entries.filterNot { types.contains(it.name) } + .map { it.getStringResource() } + PickerField( + value = typeName, + label = stringResource(R.string.monster_registration_name), + options = optionsString, + onValueChange = { optionIndex -> + onChanged( + mutableSavingThrows.changeAt(i) { + copy(type = options[optionIndex]) + } + ) + } + ) + } - if (savingThrows.size < AbilityScoreType.entries.size) { - AddButton() + formItem(key = "$key-modifier-$i") { + val typeName = savingThrow.type.toState().getStringResource() + AppTextField( + value = savingThrow.modifier, + label = typeName, + onValueChange = { newValue -> + onChanged( + mutableSavingThrows.changeAt(i) { + copy(modifier = newValue) + } + ) + } + ) + } } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpeedValuesForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpeedValuesForm.kt index aa98d288..4acec47f 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpeedValuesForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpeedValuesForm.kt @@ -1,68 +1,86 @@ package br.alexandregpereira.hunter.monster.registration.ui.form +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.model.Monster import br.alexandregpereira.hunter.domain.model.SpeedType +import br.alexandregpereira.hunter.domain.model.SpeedValue import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt import br.alexandregpereira.hunter.ui.compose.AppTextField -import br.alexandregpereira.hunter.ui.compose.Form import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterSpeedValuesForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterSpeedValuesForm( monster: Monster, - modifier: Modifier = Modifier, onMonsterChanged: (Monster) -> Unit = {} ) { val speedValues = monster.speed.values val types = speedValues.map { it.type } val options = SpeedType.entries.filterNot { types.contains(it) } - val optionsStrings = options.map { it.toTypeState().getString() } val newSpeedValues = speedValues.toMutableList() + val key = "speed" - Form( - modifier = modifier, - title = stringResource(R.string.monster_registration_speed), + FormLazy( + key = key, + title = { stringResource(R.string.monster_registration_speed) }, ) { - monster.speed.values.mapIndexed { index, speedValue -> - val name = speedValue.type.toTypeState().getString() - PickerField( - value = name, - label = stringResource(R.string.monster_registration_speed_type), - options = optionsStrings, - onValueChange = { optionIndex -> - onMonsterChanged( - monster.copy( - speed = monster.speed.copy( - values = newSpeedValues.changeAt(index) { - copy( - type = options[optionIndex], - ) - } - ) + FormItems( + key = key, + items = newSpeedValues, + createNew = { SpeedValue.create() }, + onChanged = { + onMonsterChanged( + monster.copy( + speed = monster.speed.copy( + values = it ) ) - } - ) + ) + } + ) { index, speedValue -> + formItem(key = "$key-name-$index") { + val optionsStrings = options.map { it.toTypeState().getString() } + val name = speedValue.type.toTypeState().getString() + PickerField( + value = name, + label = stringResource(R.string.monster_registration_speed_type), + options = optionsStrings, + onValueChange = { optionIndex -> + onMonsterChanged( + monster.copy( + speed = monster.speed.copy( + values = newSpeedValues.changeAt(index) { + copy( + type = options[optionIndex], + ) + } + ) + ) + ) + } + ) + } - AppTextField( - text = speedValue.valueFormatted, - label = name, - onValueChange = { newValue -> - onMonsterChanged( - monster.copy( - speed = monster.speed.copy( - values = newSpeedValues.changeAt(index) { - copy(valueFormatted = newValue) - } + formItem(key = "$key-value-$index") { + val name = speedValue.type.toTypeState().getString() + AppTextField( + text = speedValue.valueFormatted, + label = name, + onValueChange = { newValue -> + onMonsterChanged( + monster.copy( + speed = monster.speed.copy( + values = newSpeedValues.changeAt(index) { + copy(valueFormatted = newValue) + } + ) ) ) - ) - } - ) + } + ) + } } } } diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpellcastingsForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpellcastingsForm.kt index 50f50fcd..0d49ef32 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpellcastingsForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterSpellcastingsForm.kt @@ -1,8 +1,7 @@ package br.alexandregpereira.hunter.monster.registration.ui.form -import androidx.compose.foundation.clickable +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import br.alexandregpereira.hunter.domain.monster.spell.model.SpellPreview import br.alexandregpereira.hunter.domain.monster.spell.model.SpellUsage @@ -12,93 +11,117 @@ import br.alexandregpereira.hunter.monster.registration.R import br.alexandregpereira.hunter.monster.registration.ui.changeAt import br.alexandregpereira.hunter.ui.compose.AppTextField import br.alexandregpereira.hunter.ui.compose.ClickableField -import br.alexandregpereira.hunter.ui.compose.Form import br.alexandregpereira.hunter.ui.compose.PickerField -@Composable -internal fun MonsterSpellcastingsForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterSpellcastingsForm( spellcastings: List, - modifier: Modifier = Modifier, onSpellClick: (String) -> Unit = {}, onChanged: (List) -> Unit = {} -) = Form(modifier, stringResource(R.string.monster_registration_spells)) { - if (spellcastings.isEmpty()) { - AddButton(text = stringResource(R.string.monster_registration_add_spellcasting_type)) - return@Form - } - val newSpellcastings = spellcastings.toMutableList() - val options = SpellcastingType.entries - val optionStrings = SpellcastingType.entries.map { it.toState().getStringName() } - - spellcastings.forEachIndexed { index, spellcasting -> - PickerField( - value = spellcasting.type.toState().getStringName(), - label = stringResource(R.string.monster_registration_spellcasting_type_label), - options = optionStrings, - onValueChange = { optionIndex -> - onChanged(newSpellcastings.changeAt(index) { copy(type = options[optionIndex]) }) +) { + val key = "spellcastings" + FormLazy(key, { stringResource(R.string.monster_registration_spells) }) { + val newSpellcastings = spellcastings.toMutableList() + val options = SpellcastingType.entries + + FormItems( + key = key, + items = newSpellcastings, + addText = { stringResource(R.string.monster_registration_add_spellcasting_type) }, + removeText = { stringResource(R.string.monster_registration_remove_spellcasting_type) }, + createNew = { Spellcasting.create() }, + onChanged = onChanged + ) { index, spellcasting -> + formItem(key = "$key-type-$index") { + val optionStrings = SpellcastingType.entries.map { it.toState().getStringName() } + PickerField( + value = spellcasting.type.toState().getStringName(), + label = stringResource(R.string.monster_registration_spellcasting_type_label), + options = optionStrings, + onValueChange = { optionIndex -> + onChanged(newSpellcastings.changeAt(index) { copy(type = options[optionIndex]) }) + } + ) } - ) - AppTextField( - text = spellcasting.description, - label = stringResource(R.string.monster_registration_description), - multiline = true, - onValueChange = { newValue -> - onChanged(newSpellcastings.changeAt(index) { copy(description = newValue) }) + formItem(key = "$key-description-$index") { + AppTextField( + text = spellcasting.description, + label = stringResource(R.string.monster_registration_description), + multiline = true, + onValueChange = { newValue -> + onChanged(newSpellcastings.changeAt(index) { copy(description = newValue) }) + } + ) } - ) - - MonsterSpellsUsageForm( - spellsUsage = spellcasting.usages, - onSpellClick = onSpellClick, - onChanged = { newSpellsUsage -> - onChanged(newSpellcastings.changeAt(index) { copy(usages = newSpellsUsage) }) - } - ) - - AddButton(text = stringResource(R.string.monster_registration_add_spellcasting_type)) + MonsterSpellsUsageForm( + key = "$key-spellsUsage-$index", + spellsUsage = spellcasting.usages, + onSpellClick = onSpellClick, + onChanged = { newSpellsUsage -> + onChanged(newSpellcastings.changeAt(index) { copy(usages = newSpellsUsage) }) + } + ) + } } } -@Composable -internal fun MonsterSpellsUsageForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterSpellsUsageForm( + key: String, spellsUsage: List, onSpellClick: (String) -> Unit = {}, onChanged: (List) -> Unit = {} ) { val newSpellsUsage = spellsUsage.toMutableList() - - AddButton(text = stringResource(R.string.monster_registration_add_spell_group)) - - spellsUsage.forEachIndexed { index, spellUsage -> - AppTextField( - text = spellUsage.group, - label = stringResource(R.string.monster_registration_spell_group), - onValueChange = { newValue -> - onChanged(newSpellsUsage.changeAt(index) { copy(group = newValue) }) + FormItems( + key = key, + items = newSpellsUsage, + addText = { stringResource(R.string.monster_registration_add_spell_group) }, + removeText = { stringResource(R.string.monster_registration_remove_spell_group) }, + createNew = { SpellUsage.create() }, + onChanged = onChanged + ) { index, spellUsage -> + formItem(key = "$key-group-$index") { + AppTextField( + text = spellUsage.group, + label = stringResource(R.string.monster_registration_spell_group), + onValueChange = { newValue -> + onChanged(newSpellsUsage.changeAt(index) { copy(group = newValue) }) + } + ) + } + MonsterSpellsForm( + key = "$key-spells-$index", + spells = spellUsage.spells, + onSpellClick = onSpellClick, + onChanged = { newSpells -> + onChanged(newSpellsUsage.changeAt(index) { copy(spells = newSpells) }) } ) - - MonsterSpellsForm(spells = spellUsage.spells, onSpellClick = onSpellClick) - - AddButton(text = stringResource(R.string.monster_registration_add_spell_group)) } } -@Composable -internal fun MonsterSpellsForm( +@Suppress("FunctionName") +internal fun LazyListScope.MonsterSpellsForm( + key: String, spells: List, - onSpellClick: (String) -> Unit = {} -) { - spells.forEach { spell -> + onSpellClick: (String) -> Unit = {}, + onChanged: (List) -> Unit = {} +) = FormItems( + key = key, + items = spells.toMutableList(), + addText = { stringResource(R.string.monster_registration_add_spell) }, + removeText = { stringResource(R.string.monster_registration_remove_spell) }, + createNew = { SpellPreview.create() }, + onChanged = onChanged +) { index, spell -> + formItem(key = "$key-spell-name-$index") { ClickableField( text = spell.name, label = stringResource(R.string.monster_registration_spell_label), onClick = { onSpellClick(spell.index) }, ) } - - AddButton(text = stringResource(R.string.monster_registration_add_spell)) } private fun SpellcastingType.toState() = when (this) { @@ -113,4 +136,3 @@ private enum class SpellcastingTypeState(val stringRes: Int) { @Composable private fun SpellcastingTypeState.getStringName() = stringResource(stringRes) - diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStatsForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStatsForm.kt index 629d1cd0..49d9353e 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStatsForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStatsForm.kt @@ -1,61 +1,47 @@ package br.alexandregpereira.hunter.monster.registration.ui.form -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.res.stringResource -import br.alexandregpereira.hunter.domain.model.Monster +import br.alexandregpereira.hunter.domain.model.Stats import br.alexandregpereira.hunter.monster.registration.R -import br.alexandregpereira.hunter.ui.compose.Form -import br.alexandregpereira.hunter.ui.compose.FormField +import br.alexandregpereira.hunter.ui.compose.AppTextField -@Composable -internal fun MonsterStatsForm( - monster: Monster, - modifier: Modifier = Modifier, - onMonsterChanged: (Monster) -> Unit = {} +@Suppress("FunctionName") +internal fun LazyListScope.MonsterStatsForm( + stats: Stats, + onChanged: (Stats) -> Unit = {} ) { - Form( - modifier = modifier, - title = stringResource(R.string.monster_registration_stats), - formFields = listOf( - FormField.Number( - key = "armorClass", + val key = "stats" + FormLazy( + key = key, + title = { stringResource(R.string.monster_registration_stats) }, + ) { + formItem(key = "$key-armorClass") { + AppTextField( + value = stats.armorClass, label = stringResource(R.string.monster_registration_armor_class), - value = monster.stats.armorClass, - ), - FormField.Number( - key = "hitPoints", + onValueChange = { newValue -> + onChanged(stats.copy(armorClass = newValue)) + } + ) + } + formItem(key = "$key-hitPoints") { + AppTextField( + value = stats.hitPoints, label = stringResource(R.string.monster_registration_hit_points), - value = monster.stats.hitPoints, - ), - FormField.Text( - key = "hitDice", + onValueChange = { newValue -> + onChanged(stats.copy(hitPoints = newValue)) + } + ) + } + formItem(key = "$key-hitDice") { + AppTextField( + text = stats.hitDice, label = stringResource(R.string.monster_registration_hit_dice), - value = monster.stats.hitDice, - ), - ), - onFormChanged = { field -> - when (field.key) { - "armorClass" -> onMonsterChanged( - monster.copy( - stats = monster.stats.copy( - armorClass = field.intValue - ) - ) - ) - - "hitPoints" -> onMonsterChanged( - monster.copy( - stats = monster.stats.copy( - hitPoints = field.intValue - ) - ) - ) - - "hitDice" -> onMonsterChanged( - monster.copy(stats = monster.stats.copy(hitDice = field.stringValue)) - ) - } - }, - ) + onValueChange = { newValue -> + onChanged(stats.copy(hitDice = newValue)) + } + ) + } + } } \ No newline at end of file diff --git a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStringValueForm.kt b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStringValueForm.kt index 52f74af8..ead375a4 100644 --- a/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStringValueForm.kt +++ b/feature/monster-registration/android/src/main/kotlin/br/alexandregpereira/hunter/monster/registration/ui/form/MonsterStringValueForm.kt @@ -1,29 +1,24 @@ package br.alexandregpereira.hunter.monster.registration.ui.form +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import br.alexandregpereira.hunter.ui.compose.Form -import br.alexandregpereira.hunter.ui.compose.FormField +import br.alexandregpereira.hunter.ui.compose.AppTextField -@Composable -internal fun MonsterStringValueForm( - title: String, +@Suppress("FunctionName") +internal fun LazyListScope.MonsterStringValueForm( + key: String, + title: @Composable () -> String, value: String, - modifier: Modifier = Modifier, onChanged: (String) -> Unit = {} +) = FormLazy( + key = key, + title = title, ) { - Form( - modifier = modifier, - title = title, - formFields = listOf( - FormField.Text( - key = title, - label = title, - value = value, - ) - ), - onFormChanged = { field -> - onChanged(field.stringValue) - }, - ) + formItem(key = "$key-string-value") { + AppTextField( + text = value, + label = title(), + onValueChange = onChanged + ) + } } diff --git a/feature/monster-registration/android/src/main/res/drawable/ic_add.xml b/feature/monster-registration/android/src/main/res/drawable/ic_add.xml deleted file mode 100644 index 89633bb1..00000000 --- a/feature/monster-registration/android/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/feature/monster-registration/android/src/main/res/values-pt-rBR/strings.xml b/feature/monster-registration/android/src/main/res/values-pt-rBR/strings.xml index 55b4ea64..9482c38b 100644 --- a/feature/monster-registration/android/src/main/res/values-pt-rBR/strings.xml +++ b/feature/monster-registration/android/src/main/res/values-pt-rBR/strings.xml @@ -95,7 +95,20 @@ Tipo de Conjuração Grupo de Magia Magia - Adicionar grupo de magia + Adicionar grupo + Remover grupo Adicionar magia - Adicionar tipo de conjuração + Remover magia + Adicionar conjuração + Remover conjuração + Adicionar dano + Remover dano + Adicionar ação + Remover ação + Adicionar habilidade + Remover habilidade + Adicionar reação + Remover reação + Adicionar + Remover \ No newline at end of file diff --git a/feature/monster-registration/android/src/main/res/values/strings.xml b/feature/monster-registration/android/src/main/res/values/strings.xml index e3754fa7..91412b87 100644 --- a/feature/monster-registration/android/src/main/res/values/strings.xml +++ b/feature/monster-registration/android/src/main/res/values/strings.xml @@ -95,7 +95,20 @@ Spellcasting Type Spell Group Spell - Add spell group + Add group + Remove group Add spell - Add spellcasting type + Remove spell + Add spellcasting + Remove spellcasting + Add damage + Remove damage + Add action + Remove action + Add ability + Remove ability + Add reaction + Remove reaction + Add + Remove \ No newline at end of file diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationState.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationState.kt index 4f698d6b..d3b73c76 100644 --- a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationState.kt +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationState.kt @@ -7,4 +7,5 @@ data class MonsterRegistrationState( val isOpen: Boolean = false, val monster: Monster = Monster(index = ""), val initialSelectedStepIndex: Int = 0, + val isSaveButtonEnabled: Boolean = false, ) diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt index 334a4902..1a291ee0 100644 --- a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt @@ -8,6 +8,7 @@ import br.alexandregpereira.hunter.domain.usecase.GetMonsterUseCase import br.alexandregpereira.hunter.domain.usecase.SaveMonstersUseCase import br.alexandregpereira.hunter.event.EventManager import br.alexandregpereira.hunter.monster.registration.domain.NormalizeMonsterUseCase +import br.alexandregpereira.hunter.monster.registration.domain.filterEmpties import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEvent import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationResult import br.alexandregpereira.hunter.spell.compendium.event.SpellCompendiumEvent @@ -43,10 +44,12 @@ class MonsterRegistrationStateHolder internal constructor( MutableActionHandler by MutableActionHandler(), MonsterRegistrationIntent { + private var originalMonster: Monster? = null + init { observeEvents() if (state.value.isOpen) { - loadMonster() + fetchMonster() } } @@ -55,7 +58,12 @@ class MonsterRegistrationStateHolder internal constructor( } override fun onMonsterChanged(monster: Monster) { - setState { copy(monster = monster) } + setState { + copy( + monster = monster, + isSaveButtonEnabled = monster.filterEmpties() != originalMonster + ) + } } override fun onSaved() { @@ -67,9 +75,11 @@ class MonsterRegistrationStateHolder internal constructor( .flowOn(dispatcher) .onEach { onClose() - eventResultManager.dispatchEvent(MonsterRegistrationResult.OnSaved( - monsterIndex = state.value.monster.index - )) + eventResultManager.dispatchEvent( + MonsterRegistrationResult.OnSaved( + monsterIndex = state.value.monster.index + ) + ) } .launchIn(scope) } @@ -86,7 +96,7 @@ class MonsterRegistrationStateHolder internal constructor( when (result) { is SpellCompendiumResult.OnSpellClick -> updateSpells( currentSpellIndex = spellIndex, - newSpellIndex= result.spellIndex + newSpellIndex = result.spellIndex ) is SpellCompendiumResult.OnSpellLongClick -> openSpellDetail(result.spellIndex) } @@ -132,13 +142,14 @@ class MonsterRegistrationStateHolder internal constructor( spellDetailEventDispatcher.dispatchEvent(SpellDetailEvent.ShowSpell(spellIndex)) } - private fun loadMonster() { + private fun fetchMonster() { val monsterIndex = params.value.monsterIndex?.takeUnless { it.isBlank() } ?: return setState { copy(isLoading = true) } getMonster(monsterIndex) .flowOn(dispatcher) .onEach { monster -> + originalMonster = monster setState { copy(monster = monster, isLoading = false) } } .launchIn(scope) @@ -151,13 +162,14 @@ class MonsterRegistrationStateHolder internal constructor( when (event) { MonsterRegistrationEvent.Hide -> { analytics.trackMonsterRegistrationClosed(state.value.monster.index) - setState { copy(isOpen = false) } + setState { copy(isOpen = false, isSaveButtonEnabled = false) } } + is MonsterRegistrationEvent.ShowEdit -> { analytics.trackMonsterRegistrationOpened(event.monsterIndex) params.value = MonsterRegistrationParams(monsterIndex = event.monsterIndex) setState { copy(isOpen = true) } - loadMonster() + fetchMonster() } } } diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/NormalizeMonsterUseCase.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/NormalizeMonsterUseCase.kt index 5f84460e..864668da 100644 --- a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/NormalizeMonsterUseCase.kt +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/NormalizeMonsterUseCase.kt @@ -16,8 +16,13 @@ package br.alexandregpereira.hunter.monster.registration.domain +import br.alexandregpereira.hunter.domain.model.AbilityDescription +import br.alexandregpereira.hunter.domain.model.Action import br.alexandregpereira.hunter.domain.model.Color +import br.alexandregpereira.hunter.domain.model.DamageDice import br.alexandregpereira.hunter.domain.model.Monster +import br.alexandregpereira.hunter.domain.monster.spell.model.SpellUsage +import br.alexandregpereira.hunter.domain.monster.spell.model.Spellcasting import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -26,10 +31,11 @@ internal fun interface NormalizeMonsterUseCase { operator fun invoke(monster: Monster): Flow } -internal fun NormalizeMonsterUseCase(): NormalizeMonsterUseCase = NormalizeMonsterUseCase { monster -> - flowOf(monster) - .map { it.changeAbilityScoresModifier() } -} +internal fun NormalizeMonsterUseCase(): NormalizeMonsterUseCase = + NormalizeMonsterUseCase { monster -> + flowOf(monster) + .map { it.changeAbilityScoresModifier() } + } private fun Monster.changeAbilityScoresModifier(): Monster { val monster = this @@ -61,7 +67,7 @@ private fun Monster.changeAbilityScoresModifier(): Monster { imageData = monster.imageData.copy( backgroundColor = monster.imageData.backgroundColor.normalizeColor(), ), - ) + ).filterEmpties().createIndexes() } private fun Color.normalizeColor(): Color { @@ -72,3 +78,76 @@ private fun Color.normalizeColor(): Color { dark = newColor, ) } + +internal fun Monster.filterEmpties(): Monster { + val emptyAbilityDescription = AbilityDescription.create() + val emptyAction = Action.create() + + return copy( + speed = speed.copy( + values = speed.values.filter { it.valueFormatted.isNotBlank() } + ), + conditionImmunities = conditionImmunities.filter { it.name.isNotBlank() }, + savingThrows = savingThrows.filter { it.modifier != 0 }, + skills = skills.filter { it.name.isNotBlank() }, + specialAbilities = specialAbilities.filter { it != emptyAbilityDescription }, + actions = actions.filterDamageDices().filter { it != emptyAction }, + reactions = reactions.filter { it != emptyAbilityDescription }, + legendaryActions = legendaryActions.filterDamageDices().filter { it != emptyAction }, + spellcastings = spellcastings.filterSpellUsages().filter { spellcasting -> + spellcasting.usages.isNotEmpty() + } + ) +} + +private fun List.filterDamageDices(): List { + val emptyDamageDice = DamageDice.create() + return map { action -> + action.copy( + damageDices = action.damageDices.filter { it != emptyDamageDice } + ) + } +} + +private fun List.filterSpellUsages(): List { + return map { spellcasting -> + spellcasting.copy( + usages = spellcasting.usages.filterSpells().filter { it.spells.isNotEmpty() } + ) + } +} + +private fun List.filterSpells(): List { + return map { spellUsage -> + spellUsage.copy( + spells = spellUsage.spells.filter { it.name.isNotBlank() } + ) + } +} + +private fun Monster.createIndexes(): Monster { + return copy( + savingThrows = savingThrows.map { savingThrow -> + savingThrow.copy(index = "saving-throw-${savingThrow.type.name}".normalizeIndex()) + }, + skills = skills.map { skill -> + skill.copy(index = "skill-${skill.name}".normalizeIndex()) + }, + conditionImmunities = conditionImmunities.map { conditionImmunity -> + conditionImmunity.copy(index = conditionImmunity.type.name.normalizeIndex()) + }, + damageVulnerabilities = damageVulnerabilities.map { damageVulnerability -> + damageVulnerability.copy(index = damageVulnerability.type.name.normalizeIndex()) + }, + damageResistances = damageResistances.map { damageResistance -> + damageResistance.copy(index = damageResistance.type.name.normalizeIndex()) + }, + damageImmunities = damageImmunities.map { damageImmunity -> + damageImmunity.copy(index = damageImmunity.type.name.normalizeIndex()) + }, + ) +} + +private fun String.normalizeIndex(): String { + return lowercase().replace(" ", "-") +}