From 0c58b203b30f88c1b913c68ad87c7bfc96c165c5 Mon Sep 17 00:00:00 2001 From: Alexandre G Pereira Date: Thu, 11 Jul 2024 23:00:41 -0300 Subject: [PATCH] Allow edition on all monsters (#293) * Allow edition on all monsters * Remove change measurements options --- .../app/ui/AppBottomNavigationTransition.kt | 2 +- .../data/database/dao/MonsterDaoImpl.kt | 8 + .../data/database/dao/MonsterDaoMapper.kt | 8 +- .../data/database/dao/MonsterFolderDaoImpl.kt | 3 +- .../hunter/database/Monster.sq | 3 + .../hunter/domain/di/MonsterDomainModule.kt | 2 +- .../hunter/domain/model/Monster.kt | 169 +----------------- .../repository/MonsterLocalRepository.kt | 1 + .../domain/usecase/SyncMonstersUseCase.kt | 23 ++- .../monster/DefaultMonsterLocalRepository.kt | 4 + .../local/DefaultMonsterLocalDataSource.kt | 4 + .../monster/local/MonsterLocalDataSource.kt | 1 + .../data/monster/local/dao/MonsterDao.kt | 2 + .../monster/local/entity/MonsterEntity.kt | 6 +- .../local/mapper/MonsterEntityMapper.kt | 14 +- .../monster-detail/compose/build.gradle.kts | 1 + .../hunter/detail/MonsterDetailFeature.kt | 15 +- .../detail/ui/MonsterDeleteConfirmation.kt | 68 ------- .../state-holder/build.gradle.kts | 1 + .../monster/detail/MonsterDetailAnalytics.kt | 18 ++ .../detail/MonsterDetailOptionState.kt | 9 + .../monster/detail/MonsterDetailState.kt | 1 + .../detail/MonsterDetailStateHolder.kt | 55 ++++-- .../monster/detail/MonsterDetailStrings.kt | 9 + .../hunter/monster/detail/di/Module.kt | 7 +- .../detail/domain/CloneMonsterUseCase.kt | 3 +- .../detail/domain/ResetMonsterToOriginal.kt | 23 +++ .../MonsterRegistrationStateHolder.kt | 6 +- .../MonsterRegistrationStrings.kt | 4 +- .../hunter/monster/registration/di/Module.kt | 4 +- .../registration/domain/SaveMonsterUseCase.kt | 21 +++ .../hunter/settings/ui/MenuScreen.kt | 20 ++- .../ui/compose/ConfirmationBottomSheet.kt | 40 +++++ 33 files changed, 279 insertions(+), 276 deletions(-) delete mode 100644 feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt create mode 100644 feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/ResetMonsterToOriginal.kt create mode 100644 feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/SaveMonsterUseCase.kt create mode 100644 ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/ConfirmationBottomSheet.kt diff --git a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppBottomNavigationTransition.kt b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppBottomNavigationTransition.kt index 08db7790a..11115bb27 100644 --- a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppBottomNavigationTransition.kt +++ b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppBottomNavigationTransition.kt @@ -28,7 +28,7 @@ internal fun AppBottomNavigationTransition( contentPadding = contentPadding, ) BottomBarItemIcon.SETTINGS -> SettingsFeature( - versionName = "VersionName", + versionName = "", contentPadding = contentPadding, ) } diff --git a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoImpl.kt b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoImpl.kt index 7a30e53f7..10a88d4c0 100644 --- a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoImpl.kt +++ b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoImpl.kt @@ -79,6 +79,14 @@ internal class MonsterDaoImpl( .map { it.toLocalEntity() } } + override suspend fun getMonstersPreviewsEdited(): List { + return withContext(dispatcher) { + monsterQueries.getMonstersEdited() + .executeAsList() + .map { it.toLocalEntity() } + } + } + override suspend fun getMonsters(): List = withContext(dispatcher) { monsterQueries.getMonsters().executeAsList().queryMonsterCompleteEntities() } diff --git a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoMapper.kt b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoMapper.kt index fb49e44ac..fd6db66f8 100644 --- a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoMapper.kt +++ b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterDaoMapper.kt @@ -115,7 +115,11 @@ internal fun MonsterEntity.toDatabaseEntity(): MonsterDatabaseEntity { senses = this.senses, languages = this.languages, sourceName = this.sourceName, - isClone = if (this.isClone) 1L else 0L + isClone = when (this.status) { + MonsterEntityStatus.Original -> 0L + MonsterEntityStatus.Clone -> 1L + MonsterEntityStatus.Edited -> 2L + } ) } @@ -318,7 +322,7 @@ internal fun MonsterDatabaseEntity.toLocalEntity(): MonsterEntity { senses = this.senses, languages = this.languages, sourceName = this.sourceName, - isClone = this.isClone == 1L, + status = MonsterEntityStatus.entries[this.isClone.toInt()], ) } diff --git a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterFolderDaoImpl.kt b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterFolderDaoImpl.kt index f1f6a4dbe..5a8127312 100644 --- a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterFolderDaoImpl.kt +++ b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/MonsterFolderDaoImpl.kt @@ -19,6 +19,7 @@ package br.alexandregpereira.hunter.data.database.dao import br.alexandregpereira.hunter.data.monster.folder.local.dao.MonsterFolderDao import br.alexandregpereira.hunter.data.monster.folder.local.entity.MonsterFolderEntity import br.alexandregpereira.hunter.data.monster.local.entity.MonsterEntity +import br.alexandregpereira.hunter.data.monster.local.entity.MonsterEntityStatus import br.alexandregpereira.hunter.database.MonsterFolderCompleteEntityView import br.alexandregpereira.hunter.database.MonsterFolderQueries import kotlinx.coroutines.CoroutineDispatcher @@ -95,7 +96,7 @@ internal class MonsterFolderDaoImpl( senses = it.senses, languages = it.languages, sourceName = it.sourceName, - isClone = it.isClone == 1L, + status = MonsterEntityStatus.entries[it.isClone.toInt()], ) }.groupBy(keySelector = { it.first }, valueTransform = { it.second }) } diff --git a/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Monster.sq b/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Monster.sq index de32d5f68..319de05c2 100644 --- a/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Monster.sq +++ b/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Monster.sq @@ -23,3 +23,6 @@ SELECT * FROM MonsterEntity WHERE `index` == ?; getMonsters: SELECT * FROM MonsterEntity; + +getMonstersEdited: +SELECT * FROM MonsterEntity WHERE isClone == 2; diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/di/MonsterDomainModule.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/di/MonsterDomainModule.kt index 716df20ec..ddfa196e7 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/di/MonsterDomainModule.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/di/MonsterDomainModule.kt @@ -47,6 +47,6 @@ val monsterDomainModule = module { factory { SaveCompendiumScrollItemPositionUseCase(get()) } factory { SaveMeasurementUnitUseCase(get(), get()) } factory { SaveMonstersUseCase(get(), get(), get()) } - factory { SyncMonstersUseCase(get(), get(), get(), get(), get(), get()) } + factory { SyncMonstersUseCase(get(), get(), get(), get(), get(), get(), get()) } factory { GetRemoteMonstersBySourceUseCase(get(), get()) } } diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt index 479efaf60..80542350d 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt @@ -17,14 +17,8 @@ package br.alexandregpereira.hunter.domain.model import br.alexandregpereira.hunter.domain.locale.formatToNumber -import br.alexandregpereira.hunter.domain.monster.spell.model.SchoolOfMagic -import br.alexandregpereira.hunter.domain.monster.spell.model.SpellPreview -import br.alexandregpereira.hunter.domain.monster.spell.model.SpellUsage import br.alexandregpereira.hunter.domain.monster.spell.model.Spellcasting -import br.alexandregpereira.hunter.domain.monster.spell.model.SpellcastingType -import kotlin.native.ObjCName -@ObjCName(name = "Monster", exact = true) data class Monster( val index: String, val name: String = "", @@ -54,7 +48,7 @@ data class Monster( val reactions: List = emptyList(), val spellcastings: List = emptyList(), val lore: String? = null, - val isClone: Boolean = false, + val status: MonsterStatus = MonsterStatus.Original, ) { val xp: Int = challengeRatingToXp() @@ -62,6 +56,10 @@ data class Monster( val challengeRatingFormatted: String = challengeRating.getChallengeRatingFormatted() } +enum class MonsterStatus { + Original, Edited, Clone +} + private fun Float.getChallengeRatingFormatted(): String { return if (this < 1) { val value = 1 / this @@ -136,160 +134,3 @@ fun Monster.xpFormatted(): String { return "$xpString XP" } - -fun getFakeMonster(): Monster { - return Monster( - index = "1", - name = "Monster Name", - group = "Group", - stats = Stats( - armorClass = 10, - hitPoints = 10, - hitDice = "1d10", - ), - speed = Speed( - hover = false, - values = listOf( - SpeedValue( - type = SpeedType.WALK, - valueFormatted = "10 ft.", - ), - SpeedValue( - type = SpeedType.SWIM, - valueFormatted = "10 ft.", - ), - ), - ), - abilityScores = AbilityScoreType.entries.map { - AbilityScore( - type = it, - value = 10, - modifier = 0, - ) - }, - specialAbilities = listOf( - AbilityDescription( - name = "Special Ability Name", - description = "Special Ability Description", - ) - ), - actions = listOf( - Action( - id = "id", - damageDices = listOf( - DamageDice( - dice = "1d6", - damage = Damage(index = "accumsan", type = DamageType.ACID, name = "Fran Case"), - ) - ), - attackBonus = 2, - abilityDescription = AbilityDescription( - name = "Ignacio Allen", - description = "percipit" - ) - ) - ), - legendaryActions = listOf( - Action( - id = "id2", - damageDices = listOf( - DamageDice( - dice = "1d6", - damage = Damage(index = "accumsan", type = DamageType.ACID, name = "Fran Case"), - ) - ), - attackBonus = 2, - abilityDescription = AbilityDescription( - name = "Ignacio Allen", - description = "percipit" - ) - ) - ), - reactions = listOf( - AbilityDescription( - name = "Reaction Name", - description = "Reaction Description", - ) - ), - spellcastings = listOf( - Spellcasting( - description = "latine", - type = SpellcastingType.SPELLCASTER, - usages = listOf( - SpellUsage( - group = "group", - spells = listOf( - SpellPreview( - index = "index", - name = "name", - level = 1, - school = SchoolOfMagic.ABJURATION, - ) - ) - ) - ) - ) - ), - type = MonsterType.ABERRATION, - challengeRating = 10f, - imageData = MonsterImageData( - url = "http://www.bing.com/search?q=neglegentur", - backgroundColor = Color( - light = "adipisci", - dark = "libero" - ), - isHorizontal = false - ), - subtype = "subtype", - subtitle = "curae", - size = "mazim", - alignment = "conubia", - senses = listOf("senses"), - languages = "epicuri", - sourceName = "Domingo Woods", - savingThrows = listOf( - SavingThrow( - index = "index", - type = AbilityScoreType.CHARISMA, - modifier = 5 - ) - ), - skills = listOf( - Skill( - index = "index", - name = "name", - modifier = 5 - ) - ), - damageVulnerabilities = listOf( - Damage( - index = "index", - type = DamageType.ACID, - name = "name" - ) - ), - damageResistances = listOf( - Damage( - index = "index", - type = DamageType.ACID, - name = "name" - ) - ), - damageImmunities = listOf( - Damage( - index = "index", - type = DamageType.ACID, - name = "name" - ) - ), - conditionImmunities = listOf( - Condition( - index = "index", - name = "name", - type = ConditionType.BLINDED - ) - ), - lore = "lore", - isClone = true, - ) -} diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/repository/MonsterLocalRepository.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/repository/MonsterLocalRepository.kt index 0210277a3..af9ef62ce 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/repository/MonsterLocalRepository.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/repository/MonsterLocalRepository.kt @@ -23,6 +23,7 @@ interface MonsterLocalRepository { fun saveMonsters(monsters: List, isSync: Boolean = false): Flow fun getMonsterPreviews(): Flow> + fun getMonsterPreviewsEdited(): Flow> fun getMonsters(): Flow> fun getMonsters(indexes: List): Flow> fun getMonster(index: String): Flow diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SyncMonstersUseCase.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SyncMonstersUseCase.kt index 5b4d73ef3..344fd75a9 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SyncMonstersUseCase.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SyncMonstersUseCase.kt @@ -20,7 +20,8 @@ import br.alexandregpereira.hunter.domain.model.Monster import br.alexandregpereira.hunter.domain.model.MonsterImage import br.alexandregpereira.hunter.domain.model.MonsterSource import br.alexandregpereira.hunter.domain.repository.MonsterAlternativeSourceRepository -import br.alexandregpereira.hunter.domain.repository.MonsterRepository +import br.alexandregpereira.hunter.domain.repository.MonsterLocalRepository +import br.alexandregpereira.hunter.domain.repository.MonsterRemoteRepository import br.alexandregpereira.hunter.domain.repository.MonsterSettingsRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -30,10 +31,12 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.reduce +import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.zip class SyncMonstersUseCase internal constructor( - private val repository: MonsterRepository, + private val remoteRepository: MonsterRemoteRepository, + private val localRepository: MonsterLocalRepository, private val alternativeSourceRepository: MonsterAlternativeSourceRepository, private val monsterSettingsRepository: MonsterSettingsRepository, private val getMonsterImages: GetMonsterImagesUseCase, @@ -56,8 +59,9 @@ class SyncMonstersUseCase internal constructor( } .reduce { accumulator, value -> accumulator + value } } - .flatMapLatest { - saveMonstersUseCase(monsters = it, isSync = true) + .filterMonstersNotEdited() + .flatMapLatest { monsters -> + saveMonstersUseCase(monsters = monsters, isSync = true) }.flatMapLatest { saveCompendiumScrollItemPositionUseCase(position = 0) } @@ -67,11 +71,11 @@ class SyncMonstersUseCase internal constructor( private fun MonsterSource.getRemoteMonsters(monsterImages: List): Flow> { return if (this == srdSource) { monsterSettingsRepository.getLanguage().flatMapLatest { - repository.getRemoteMonsters(lang = it) + remoteRepository.getMonsters(lang = it) } } else { monsterSettingsRepository.getLanguage().flatMapLatest { - repository.getRemoteMonsters( + remoteRepository.getMonsters( sourceAcronym = this.acronym, lang = it ).catch { error -> @@ -87,4 +91,11 @@ class SyncMonstersUseCase internal constructor( ): Flow> = map { it.appendMonsterImages(monsterImages) } + + private fun Flow>.filterMonstersNotEdited(): Flow> = map { monsters -> + val monstersEditedIndexes = localRepository.getMonsterPreviewsEdited().single() + .map { it.index } + .toSet() + monsters.filterNot { monstersEditedIndexes.contains(it.index) } + } } diff --git a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/DefaultMonsterLocalRepository.kt b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/DefaultMonsterLocalRepository.kt index 4db55deba..32a8b9802 100644 --- a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/DefaultMonsterLocalRepository.kt +++ b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/DefaultMonsterLocalRepository.kt @@ -37,6 +37,10 @@ internal class DefaultMonsterLocalRepository( return localDataSource.getMonsterPreviews().map { it.toDomainMonsterEntity() } } + override fun getMonsterPreviewsEdited(): Flow> { + return localDataSource.getMonsterPreviewsEdited().map { it.toDomainMonsterEntity() } + } + override fun getMonsters(): Flow> { return localDataSource.getMonsters().map { it.toDomain() } } diff --git a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/DefaultMonsterLocalDataSource.kt b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/DefaultMonsterLocalDataSource.kt index 9897eae39..7afcea57e 100644 --- a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/DefaultMonsterLocalDataSource.kt +++ b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/DefaultMonsterLocalDataSource.kt @@ -38,6 +38,10 @@ internal class DefaultMonsterLocalDataSource( } } + override fun getMonsterPreviewsEdited(): Flow> = flow { + emit(mutex.withLock { monsterDao.getMonstersPreviewsEdited() }) + } + override fun getMonsters(): Flow> = flow { mutex.withLock { monsterDao.getMonsters() diff --git a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/MonsterLocalDataSource.kt b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/MonsterLocalDataSource.kt index 638f329b3..ead09ad11 100644 --- a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/MonsterLocalDataSource.kt +++ b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/MonsterLocalDataSource.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.Flow internal interface MonsterLocalDataSource { fun getMonsterPreviews(): Flow> + fun getMonsterPreviewsEdited(): Flow> fun getMonsters(): Flow> fun getMonsters(indexes: List): Flow> fun getMonstersByQuery(query: String): Flow> diff --git a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/dao/MonsterDao.kt b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/dao/MonsterDao.kt index fcde90cc3..b20fe5afa 100644 --- a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/dao/MonsterDao.kt +++ b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/dao/MonsterDao.kt @@ -25,6 +25,8 @@ interface MonsterDao { suspend fun getMonsterPreviews(indexes: List): List + suspend fun getMonstersPreviewsEdited(): List + suspend fun getMonsters(): List suspend fun getMonsters(indexes: List): List diff --git a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/entity/MonsterEntity.kt b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/entity/MonsterEntity.kt index cecd8eac8..08d0dcef3 100644 --- a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/entity/MonsterEntity.kt +++ b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/entity/MonsterEntity.kt @@ -36,5 +36,9 @@ data class MonsterEntity( val senses: String, val languages: String, val sourceName: String, - val isClone: Boolean + val status: MonsterEntityStatus, ) + +enum class MonsterEntityStatus { + Original, Clone, Edited +} diff --git a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/mapper/MonsterEntityMapper.kt b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/mapper/MonsterEntityMapper.kt index a9217dae9..f3b65a078 100644 --- a/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/mapper/MonsterEntityMapper.kt +++ b/domain/monster/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/local/mapper/MonsterEntityMapper.kt @@ -18,11 +18,13 @@ package br.alexandregpereira.hunter.data.monster.local.mapper import br.alexandregpereira.hunter.data.monster.local.entity.MonsterCompleteEntity import br.alexandregpereira.hunter.data.monster.local.entity.MonsterEntity +import br.alexandregpereira.hunter.data.monster.local.entity.MonsterEntityStatus import br.alexandregpereira.hunter.data.monster.spell.local.mapper.toDomain import br.alexandregpereira.hunter.data.monster.spell.local.mapper.toEntity import br.alexandregpereira.hunter.domain.model.Color import br.alexandregpereira.hunter.domain.model.Monster import br.alexandregpereira.hunter.domain.model.MonsterImageData +import br.alexandregpereira.hunter.domain.model.MonsterStatus import br.alexandregpereira.hunter.domain.model.MonsterType import br.alexandregpereira.hunter.domain.model.Speed import br.alexandregpereira.hunter.domain.model.Stats @@ -80,7 +82,11 @@ internal fun Monster.toEntity(): MonsterCompleteEntity { senses = senses.joinToString(), languages = languages, sourceName = sourceName, - isClone = isClone, + status = when (status) { + MonsterStatus.Original -> MonsterEntityStatus.Original + MonsterStatus.Clone -> MonsterEntityStatus.Clone + MonsterStatus.Edited -> MonsterEntityStatus.Edited + }, ), speed = speed.toEntity(index), abilityScores = toAbilityScoreEntity(), @@ -130,6 +136,10 @@ private fun MonsterEntity.toDomain(): Monster { sourceName = monster.sourceName, senses = monster.senses.split(", "), languages = monster.languages, - isClone = monster.isClone + status = when (monster.status) { + MonsterEntityStatus.Original -> MonsterStatus.Original + MonsterEntityStatus.Clone -> MonsterStatus.Clone + MonsterEntityStatus.Edited -> MonsterStatus.Edited + } ) } diff --git a/feature/monster-detail/compose/build.gradle.kts b/feature/monster-detail/compose/build.gradle.kts index e7749ec25..ea42d5ad4 100644 --- a/feature/monster-detail/compose/build.gradle.kts +++ b/feature/monster-detail/compose/build.gradle.kts @@ -18,6 +18,7 @@ multiplatform { implementation(project(":feature:monster-lore-detail:event")) implementation(project(":feature:monster-registration:event")) implementation(project(":feature:spell-detail:event")) + implementation(project(":feature:sync:event")) implementation(project(":feature:monster-detail:state-holder")) implementation(project(":ui:core")) diff --git a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt index 6c86dc23a..f0800f6ce 100644 --- a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt +++ b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt @@ -27,13 +27,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import br.alexandregpereira.hunter.detail.ui.LocalStrings -import br.alexandregpereira.hunter.detail.ui.MonsterDeleteConfirmation import br.alexandregpereira.hunter.detail.ui.MonsterDetailOptionPicker import br.alexandregpereira.hunter.detail.ui.MonsterDetailScreen import br.alexandregpereira.hunter.detail.ui.strings import br.alexandregpereira.hunter.monster.detail.MonsterDetailStateHolder import br.alexandregpereira.hunter.monster.detail.di.MonsterDetailStateRecoveryQualifier import br.alexandregpereira.hunter.ui.compose.BackHandler +import br.alexandregpereira.hunter.ui.compose.ConfirmationBottomSheet import br.alexandregpereira.hunter.ui.compose.FormBottomSheet import br.alexandregpereira.hunter.ui.compose.FormField import br.alexandregpereira.hunter.ui.compose.LoadingScreen @@ -112,12 +112,23 @@ fun MonsterDetailFeature( onSaved = viewModel::onCloneFormSaved, ) - MonsterDeleteConfirmation( + ConfirmationBottomSheet( show = viewState.showDeleteConfirmation, + description = strings.deleteQuestion, + buttonText = strings.deleteConfirmation, contentPadding = contentPadding, onConfirmed = viewModel::onDeleteConfirmed, onClosed = viewModel::onDeleteClosed ) + + ConfirmationBottomSheet( + show = viewState.showResetConfirmation, + description = strings.resetQuestion, + buttonText = strings.resetConfirmation, + contentPadding = contentPadding, + onConfirmed = viewModel::onResetConfirmed, + onClosed = viewModel::onResetClosed + ) } } } diff --git a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt deleted file mode 100644 index f4bb65415..000000000 --- a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2022 Alexandre Gomes Pereira - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package br.alexandregpereira.hunter.detail.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import org.jetbrains.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import br.alexandregpereira.hunter.ui.compose.AppButton -import br.alexandregpereira.hunter.ui.compose.BottomSheet -import br.alexandregpereira.hunter.ui.compose.ScreenHeader - -@Composable -internal fun MonsterDeleteConfirmation( - show: Boolean, - contentPadding: PaddingValues = PaddingValues(), - onConfirmed: () -> Unit = {}, - onClosed: () -> Unit = {} -) = BottomSheet( - opened = show, - contentPadding = PaddingValues( - top = 16.dp + contentPadding.calculateTopPadding(), - bottom = 16.dp + contentPadding.calculateBottomPadding(), - start = 16.dp, - end = 16.dp, - ), - onClose = onClosed, -) { - Spacer(modifier = Modifier.height(16.dp)) - - ScreenHeader( - title = strings.deleteQuestion, - ) - - Spacer(modifier = Modifier.height(32.dp)) - - AppButton( - text = strings.deleteConfirmation, - onClick = onConfirmed - ) -} - -@Preview -@Composable -private fun MonsterDeleteConfirmationPreview() { - MonsterDeleteConfirmation( - show = true, - onConfirmed = {}, - onClosed = {} - ) -} \ No newline at end of file diff --git a/feature/monster-detail/state-holder/build.gradle.kts b/feature/monster-detail/state-holder/build.gradle.kts index d381e483c..d5939542e 100644 --- a/feature/monster-detail/state-holder/build.gradle.kts +++ b/feature/monster-detail/state-holder/build.gradle.kts @@ -32,6 +32,7 @@ multiplatform { implementation(project(":feature:monster-lore-detail:event")) implementation(project(":feature:monster-registration:event")) implementation(project(":feature:spell-detail:event")) + implementation(project(":feature:sync:event")) implementation(libs.kotlin.coroutines.core) implementation(libs.koin.core) } diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt index c6057fc01..af33c362e 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt @@ -133,4 +133,22 @@ class MonsterDetailAnalytics( ) ) } + + fun trackMonsterDetailResetToOriginalClicked(monsterIndex: String) { + analytics.track( + eventName = "MonsterDetail - reset to original clicked", + params = mapOf( + "monsterIndex" to monsterIndex, + ) + ) + } + + fun trackMonsterDetailResetConfirmed(monsterIndex: String) { + analytics.track( + eventName = "MonsterDetail - reset confirmed", + params = mapOf( + "monsterIndex" to monsterIndex, + ) + ) + } } \ No newline at end of file diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailOptionState.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailOptionState.kt index f3b55ec80..87bde29ef 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailOptionState.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailOptionState.kt @@ -31,6 +31,15 @@ data class MonsterDetailOptionState( internal const val DELETE = "delete" internal const val CHANGE_TO_FEET = "change_to_feet" internal const val CHANGE_TO_METERS = "change_to_meters" + internal const val RESET_TO_ORIGINAL = "reset_to_original" + + @Suppress("FunctionName") + internal fun ResetToOriginal(strings: MonsterDetailStrings): MonsterDetailOptionState { + return MonsterDetailOptionState( + id = RESET_TO_ORIGINAL, + name = strings.resetToOriginal + ) + } @Suppress("FunctionName") internal fun AddToFolder(strings: MonsterDetailStrings): MonsterDetailOptionState { diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt index 967078d9d..97e486c00 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt @@ -36,6 +36,7 @@ data class MonsterDetailState( val showCloneForm: Boolean = false, val monsterCloneName: String = "", val showDeleteConfirmation: Boolean = false, + val showResetConfirmation: Boolean = false, val strings: MonsterDetailStrings = MonsterDetailStrings(), ) { diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt index ca19ab223..db0c42406 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt @@ -18,6 +18,7 @@ package br.alexandregpereira.hunter.monster.detail import br.alexandregpereira.hunter.domain.model.MeasurementUnit import br.alexandregpereira.hunter.domain.model.Monster +import br.alexandregpereira.hunter.domain.model.MonsterStatus import br.alexandregpereira.hunter.domain.usecase.ChangeMonstersMeasurementUnitUseCase import br.alexandregpereira.hunter.event.EventDispatcher import br.alexandregpereira.hunter.event.EventListener @@ -38,16 +39,17 @@ import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Compa import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.CHANGE_TO_FEET import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.CHANGE_TO_METERS import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.CLONE -import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.ChangeToFeet -import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.ChangeToMeters import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.Clone import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.DELETE import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.Delete import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.EDIT import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.Edit +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.RESET_TO_ORIGINAL +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.ResetToOriginal import br.alexandregpereira.hunter.monster.detail.domain.CloneMonsterUseCase import br.alexandregpereira.hunter.monster.detail.domain.DeleteMonsterUseCase import br.alexandregpereira.hunter.monster.detail.domain.GetMonsterDetailUseCase +import br.alexandregpereira.hunter.monster.detail.domain.ResetMonsterToOriginal import br.alexandregpereira.hunter.monster.detail.domain.model.MonsterDetail import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEvent import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationResult @@ -55,6 +57,9 @@ import br.alexandregpereira.hunter.monster.registration.event.collectOnSaved import br.alexandregpereira.hunter.spell.detail.event.SpellDetailEvent import br.alexandregpereira.hunter.spell.detail.event.SpellDetailEventDispatcher import br.alexandregpereira.hunter.state.UiModel +import br.alexandregpereira.hunter.sync.event.SyncEventDispatcher +import br.alexandregpereira.hunter.sync.event.SyncEventListener +import br.alexandregpereira.hunter.sync.event.collectSyncFinishedEvents import br.alexandregpereira.hunter.ui.StateRecovery import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -70,14 +75,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlin.native.ObjCName -@ObjCName(name = "MonsterDetailStateHolder", exact = true) class MonsterDetailStateHolder internal constructor( private val getMonsterDetailUseCase: GetMonsterDetailUseCase, private val cloneMonster: CloneMonsterUseCase, private val changeMonstersMeasurementUnitUseCase: ChangeMonstersMeasurementUnitUseCase, private val deleteMonster: DeleteMonsterUseCase, + private val resetMonsterToOriginal: ResetMonsterToOriginal, private val spellDetailEventDispatcher: SpellDetailEventDispatcher, private val monsterDetailEventListener: MonsterDetailEventListener, private val monsterDetailEventDispatcher: MonsterDetailEventDispatcher, @@ -85,6 +89,8 @@ class MonsterDetailStateHolder internal constructor( private val folderInsertEventDispatcher: FolderInsertEventDispatcher, private val monsterRegistrationEventDispatcher: EventDispatcher, private val monsterRegistrationEventListener: EventListener, + private val syncEventDispatcher: SyncEventDispatcher, + private val syncEventListener: SyncEventListener, private val dispatcher: CoroutineDispatcher, private val analytics: MonsterDetailAnalytics, private val appLocalization: AppLocalization, @@ -137,6 +143,10 @@ class MonsterDetailStateHolder internal constructor( monsterRegistrationEventListener.collectOnSaved { getMonstersByInitialIndex(monsterIndex, monsterIndexes, invalidateCache = true) }.launchIn(scope) + + syncEventListener.collectSyncFinishedEvents { + getMonstersByInitialIndex(monsterIndex, monsterIndexes, invalidateCache = true) + }.launchIn(scope) } private fun getMonstersByInitialIndex( @@ -191,6 +201,11 @@ class MonsterDetailStateHolder internal constructor( setState { copy(showDeleteConfirmation = true) } } + RESET_TO_ORIGINAL -> { + analytics.trackMonsterDetailResetToOriginalClicked(monsterIndex) + setState { copy(showResetConfirmation = true) } + } + CHANGE_TO_FEET -> { changeMeasurementUnit(MeasurementUnit.FEET) } @@ -242,6 +257,16 @@ class MonsterDetailStateHolder internal constructor( setState { copy(showDeleteConfirmation = false) } } + fun onResetConfirmed() { + analytics.trackMonsterDetailResetConfirmed(monsterIndex) + setState { copy(showResetConfirmation = false) } + resetMonster() + } + + fun onResetClosed() { + setState { copy(showResetConfirmation = false) } + } + private fun getMonsterDetail( monsterIndex: String = this.monsterIndex, invalidateCache: Boolean = false @@ -306,15 +331,14 @@ class MonsterDetailStateHolder internal constructor( private fun MonsterDetailState.changeOptions(): MonsterDetailState { val monster = metadata.find { monster -> monster.index == monsterIndex } ?: return this - val editOption = if (monster.isClone) { - listOf(Edit(strings), Delete(strings)) - } else emptyList() + val editOption = when (monster.status) { + MonsterStatus.Original -> listOf(Edit(strings)) + MonsterStatus.Clone -> listOf(Edit(strings), Delete(strings)) + MonsterStatus.Edited -> listOf(Edit(strings), ResetToOriginal(strings)) + } return copy( - options = listOf(AddToFolder(strings), Clone(strings)) + editOption + when (measurementUnit) { - MeasurementUnit.FEET -> ChangeToFeet(strings) - MeasurementUnit.METER -> ChangeToMeters(strings) - } + options = listOf(AddToFolder(strings), Clone(strings)) + editOption ) } @@ -358,4 +382,13 @@ class MonsterDetailStateHolder internal constructor( } .launchIn(scope) } + + private fun resetMonster() { + resetMonsterToOriginal(monsterIndex) + .flowOn(dispatcher) + .onEach { + syncEventDispatcher.startSync() + } + .launchIn(scope) + } } diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStrings.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStrings.kt index f998be874..6c133ae08 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStrings.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStrings.kt @@ -44,6 +44,9 @@ interface MonsterDetailStrings { val deleteQuestion: String val deleteConfirmation: String val save: String + val resetToOriginal: String + val resetQuestion: String + val resetConfirmation: String } internal data class MonsterDetailEnStrings( @@ -85,6 +88,9 @@ internal data class MonsterDetailEnStrings( override val deleteQuestion: String = "Are you sure you want to delete this monster?", override val deleteConfirmation: String = "I'm sure", override val save: String = "Save", + override val resetToOriginal: String = "Reset to Original", + override val resetQuestion: String = "Are you sure you want to reset this monster to its original state?", + override val resetConfirmation: String = "I'm sure", ) : MonsterDetailStrings internal data class MonsterDetailPtStrings( @@ -126,6 +132,9 @@ internal data class MonsterDetailPtStrings( override val deleteQuestion: String = "Tem certeza que deseja excluir esse monstro?", override val deleteConfirmation: String = "Tenho certeza", override val save: String = "Salvar", + override val resetToOriginal: String = "Restaurar para o Original", + override val resetQuestion: String = "Tem certeza que deseja restaurar esse monstro para o estado original?", + override val resetConfirmation: String = "Tenho certeza", ) : MonsterDetailStrings fun MonsterDetailStrings(): MonsterDetailStrings = MonsterDetailEnStrings() diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/di/Module.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/di/Module.kt index a049f7835..74c643c01 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/di/Module.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/di/Module.kt @@ -24,6 +24,7 @@ import br.alexandregpereira.hunter.monster.detail.MonsterDetailStateHolder import br.alexandregpereira.hunter.monster.detail.domain.CloneMonsterUseCase import br.alexandregpereira.hunter.monster.detail.domain.DeleteMonsterUseCase import br.alexandregpereira.hunter.monster.detail.domain.GetMonsterDetailUseCase +import br.alexandregpereira.hunter.monster.detail.domain.ResetMonsterToOriginal import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEventDispatcher import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEventListener import br.alexandregpereira.hunter.ui.StateRecovery @@ -58,11 +59,15 @@ val monsterDetailModule = module { dispatcher = get(), analytics = MonsterDetailAnalytics(get()), appLocalization = get(), - stateRecovery = get(named(MonsterDetailStateRecoveryQualifier)) + stateRecovery = get(named(MonsterDetailStateRecoveryQualifier)), + resetMonsterToOriginal = get(), + syncEventDispatcher = get(), + syncEventListener = get(), ) } factory { CloneMonsterUseCase(get(), get(), get(), get()) } factory { DeleteMonsterUseCase(repository = get()) } + factory { ResetMonsterToOriginal(get(), get()) } } const val MonsterDetailStateRecoveryQualifier = "MonsterDetailStateRecovery" diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/CloneMonsterUseCase.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/CloneMonsterUseCase.kt index 5afa738e3..126167453 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/CloneMonsterUseCase.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/CloneMonsterUseCase.kt @@ -1,5 +1,6 @@ package br.alexandregpereira.hunter.monster.detail.domain +import br.alexandregpereira.hunter.domain.model.MonsterStatus import br.alexandregpereira.hunter.domain.monster.lore.GetMonsterLoreUseCase import br.alexandregpereira.hunter.domain.monster.lore.SaveMonstersLoreUseCase import br.alexandregpereira.hunter.domain.usecase.GetMonsterUseCase @@ -25,7 +26,7 @@ internal fun CloneMonsterUseCase( monster.copy( index = "$monsterNameIndex-$monsterIndex-k4k4sh1", name = monsterName, - isClone = true, + status = MonsterStatus.Clone, ) } .map { monster -> diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/ResetMonsterToOriginal.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/ResetMonsterToOriginal.kt new file mode 100644 index 000000000..c7237c676 --- /dev/null +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/domain/ResetMonsterToOriginal.kt @@ -0,0 +1,23 @@ +package br.alexandregpereira.hunter.monster.detail.domain + +import br.alexandregpereira.hunter.domain.model.MonsterStatus +import br.alexandregpereira.hunter.domain.usecase.GetMonsterUseCase +import br.alexandregpereira.hunter.domain.usecase.SaveMonstersUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.single + +internal fun interface ResetMonsterToOriginal { + operator fun invoke(monsterIndex: String): Flow +} + +internal fun ResetMonsterToOriginal( + getMonster: GetMonsterUseCase, + saveMonstersUseCase: SaveMonstersUseCase, +) = ResetMonsterToOriginal { monsterIndex -> + getMonster(monsterIndex).map { monster -> + monster.copy(status = MonsterStatus.Original) + }.map { newMonster -> + saveMonstersUseCase(listOf(newMonster)).single() + } +} 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 ad898486b..8046fa1ed 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,10 +8,10 @@ import br.alexandregpereira.hunter.domain.model.Monster import br.alexandregpereira.hunter.domain.monster.spell.model.SchoolOfMagic import br.alexandregpereira.hunter.domain.spell.GetSpellUseCase import br.alexandregpereira.hunter.domain.usecase.GetMonsterUseCase -import br.alexandregpereira.hunter.domain.usecase.SaveMonstersUseCase import br.alexandregpereira.hunter.event.EventManager import br.alexandregpereira.hunter.localization.AppLocalization import br.alexandregpereira.hunter.monster.registration.domain.NormalizeMonsterUseCase +import br.alexandregpereira.hunter.monster.registration.domain.SaveMonsterUseCase import br.alexandregpereira.hunter.monster.registration.domain.filterEmpties import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEvent import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationResult @@ -43,7 +43,7 @@ class MonsterRegistrationStateHolder internal constructor( private val eventManager: EventManager, private val eventResultManager: EventManager, private val getMonster: GetMonsterUseCase, - private val saveMonsters: SaveMonstersUseCase, + private val saveMonster: SaveMonsterUseCase, private val normalizeMonster: NormalizeMonsterUseCase, private val analytics: Analytics, private val spellCompendiumEventDispatcher: SpellCompendiumEventResultDispatcher, @@ -78,7 +78,7 @@ class MonsterRegistrationStateHolder internal constructor( normalizeMonster(metadata.monster) .flatMapConcat { monster -> analytics.trackMonsterRegistrationSaved(monster) - saveMonsters(monsters = listOf(monster)) + saveMonster(monster) } .flowOn(dispatcher) .onEach { diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStrings.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStrings.kt index 13b3338de..a2fb78251 100644 --- a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStrings.kt +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStrings.kt @@ -168,7 +168,7 @@ internal data class MonsterRegistrationEnStrings( override val damageType: String = "Damage Type", override val damageDice: String = "Damage Dice", override val conditionType: String = "Condition Type", - override val edit: String = "Edit (Beta)", + override val edit: String = "Edit", override val group: String = "Group", override val imageUrl: String = "Image Url", override val type: String = "Type", @@ -276,7 +276,7 @@ internal data class MonsterRegistrationPtStrings( override val damageType: String = "Tipo de Dano", override val damageDice: String = "Dados de Dano", override val conditionType: String = "Tipo de Condição", - override val edit: String = "Editar (Beta)", + override val edit: String = "Editar", override val group: String = "Grupo", override val imageUrl: String = "Url da Imagem", override val type: String = "Tipo", diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/di/Module.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/di/Module.kt index 42d561df7..afc5abc1c 100644 --- a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/di/Module.kt +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/di/Module.kt @@ -6,6 +6,7 @@ import br.alexandregpereira.hunter.monster.registration.MonsterRegistrationState import br.alexandregpereira.hunter.monster.registration.di.MonsterRegistrationQualifiers.eventManagerQualifier import br.alexandregpereira.hunter.monster.registration.di.MonsterRegistrationQualifiers.paramsQualifier import br.alexandregpereira.hunter.monster.registration.domain.NormalizeMonsterUseCase +import br.alexandregpereira.hunter.monster.registration.domain.SaveMonsterUseCase import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEvent import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEventDispatcher import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEventListener @@ -37,6 +38,7 @@ val monsterRegistrationModule = module { get() } factory { NormalizeMonsterUseCase() } + factory { SaveMonsterUseCase(get()) } single { MonsterRegistrationStateHolder( @@ -45,7 +47,7 @@ val monsterRegistrationModule = module { eventResultManager = get(), dispatcher = Dispatchers.Default, getMonster = get(), - saveMonsters = get(), + saveMonster = get(), normalizeMonster = get(), analytics = get(), spellCompendiumEventDispatcher = get(), diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/SaveMonsterUseCase.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/SaveMonsterUseCase.kt new file mode 100644 index 000000000..4a50b5d4a --- /dev/null +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/domain/SaveMonsterUseCase.kt @@ -0,0 +1,21 @@ +package br.alexandregpereira.hunter.monster.registration.domain + +import br.alexandregpereira.hunter.domain.model.Monster +import br.alexandregpereira.hunter.domain.model.MonsterStatus +import br.alexandregpereira.hunter.domain.usecase.SaveMonstersUseCase +import kotlinx.coroutines.flow.Flow + +internal fun interface SaveMonsterUseCase { + operator fun invoke(monster: Monster): Flow +} + +internal fun SaveMonsterUseCase( + saveMonsters: SaveMonstersUseCase, +) = SaveMonsterUseCase { monster -> + val newMonster = when (monster.status) { + MonsterStatus.Original -> monster.copy(status = MonsterStatus.Edited) + MonsterStatus.Edited, + MonsterStatus.Clone -> monster + } + saveMonsters(listOf(newMonster)) +} diff --git a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/ui/MenuScreen.kt b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/ui/MenuScreen.kt index 12e7546f1..fa0453c0e 100644 --- a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/ui/MenuScreen.kt +++ b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/ui/MenuScreen.kt @@ -70,15 +70,17 @@ internal fun MenuScreen( ) } - Text( - text = "v$versionName", - fontWeight = FontWeight.Light, - fontSize = 12.sp, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(contentPadding) - .padding(8.dp) - ) + if (versionName.isNotBlank()) { + Text( + text = "v$versionName", + fontWeight = FontWeight.Light, + fontSize = 12.sp, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(contentPadding) + .padding(8.dp) + ) + } BottomSheet( opened = state.advancedSettingsOpened, diff --git a/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/ConfirmationBottomSheet.kt b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/ConfirmationBottomSheet.kt new file mode 100644 index 000000000..ea06e79f7 --- /dev/null +++ b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/ConfirmationBottomSheet.kt @@ -0,0 +1,40 @@ +package br.alexandregpereira.hunter.ui.compose + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ConfirmationBottomSheet( + show: Boolean, + description: String, + buttonText: String, + contentPadding: PaddingValues = PaddingValues(), + onConfirmed: () -> Unit = {}, + onClosed: () -> Unit = {} +) = BottomSheet( + opened = show, + contentPadding = PaddingValues( + top = 16.dp + contentPadding.calculateTopPadding(), + bottom = 16.dp + contentPadding.calculateBottomPadding(), + start = 16.dp, + end = 16.dp, + ), + onClose = onClosed, +) { + Spacer(modifier = Modifier.height(16.dp)) + + ScreenHeader( + title = description, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + AppButton( + text = buttonText, + onClick = onConfirmed + ) +}