From f305339af0a4af817f427a5e78b6146735e9aa8e Mon Sep 17 00:00:00 2001 From: Alexandre G Pereira Date: Fri, 19 Jul 2024 19:28:32 -0300 Subject: [PATCH] Implement share content feature (#303) * Implement share content feature * fix text field max width size constraint * Remove name field from SharedMonsterLore * Improve contentToImportShort * Fix jvm migration and sync monster after imported creatures * Fix jvm database migration when has no database --- app/build.gradle.kts | 2 + .../hunter/app/MainViewModel.kt | 3 + .../hunter/app/di/AppModule.kt | 5 + .../hunter/app/event/AppEventDispatcher.kt | 33 ++++ .../alexandregpereira/hunter/app/event/di.kt | 7 + .../hunter/app/ui/AppMainScreen.kt | 6 + .../hunter/event/v2/Event.kt | 30 ++++ .../data/database/dao/MonsterDaoMapper.kt | 1 + .../hunter/data/database/dao/SpellDaoImpl.kt | 6 +- .../hunter/database/Monster.sq | 2 +- .../hunter/database/Spell.sq | 4 +- .../src/commonMain/sqldelight/databases/26.db | Bin 0 -> 303104 bytes .../commonMain/sqldelight/migrations/25.sqm | 1 + .../hunter/data/di/JvmSqlDriverFactory.kt | 44 ++++- .../monster/lore/local/dao/MonsterLoreDao.kt | 2 - .../hunter/domain/model/AbilityDescription.kt | 3 - .../hunter/domain/model/AbilityScore.kt | 4 - .../hunter/domain/model/Action.kt | 3 - .../hunter/domain/model/Condition.kt | 3 - .../hunter/domain/model/Damage.kt | 4 - .../hunter/domain/model/Monster.kt | 2 +- .../hunter/domain/model/MonsterType.kt | 3 - .../hunter/domain/model/SavingThrow.kt | 2 - .../hunter/domain/model/Skill.kt | 2 - .../hunter/domain/model/SpeedValue.kt | 6 - .../hunter/domain/model/Stats.kt | 3 - .../monster/spell/model/SpellPreview.kt | 3 - .../domain/monster/spell/model/SpellUsage.kt | 2 - .../monster/spell/model/Spellcasting.kt | 2 - .../monster/spell/model/SpellcastingType.kt | 2 - .../domain/usecase/SaveMonstersUseCase.kt | 20 ++- .../domain/usecase/SyncMonstersUseCase.kt | 4 +- .../monster/local/entity/MonsterEntity.kt | 2 +- .../local/mapper/MonsterEntityMapper.kt | 2 + .../hunter/domain/spell/SaveSpellsUseCase.kt | 14 ++ .../hunter/domain/spell/di/DomainModule.kt | 2 + .../hunter/domain/spell/model/Spell.kt | 8 +- .../data/src/androidMain/AndroidManifest.xml | 18 -- .../hunter/data/spell/di/AndroidDataModule.kt | 30 ---- .../hunter/data/spell/di/DataModule.kt | 10 +- .../spell/local/mapper/SpellEntityMapper.kt | 13 +- .../data/spell/local/model/SpellEntity.kt | 3 +- .../spell/remote/mapper/SpellDtoMapper.kt | 4 +- .../hunter/data/spell/di/IosModule.kt | 30 ---- .../hunter/data/spell/di/JvmModule.kt | 30 ---- .../folder/insert/ui/FolderInsertScreen.kt | 3 +- .../hunter/detail/MonsterDetailFeature.kt | 3 +- .../detail/ui/MonsterDetailOptionPicker.kt | 6 +- .../state-holder/build.gradle.kts | 1 + .../monster/detail/MonsterDetailAnalytics.kt | 9 + .../detail/MonsterDetailOptionState.kt | 35 +++- .../detail/MonsterDetailStateHolder.kt | 29 +++- .../monster/detail/MonsterDetailStrings.kt | 3 + .../hunter/monster/detail/di/Module.kt | 1 + .../registration/domain/SaveMonsterUseCase.kt | 1 + feature/settings/compose/build.gradle.kts | 1 + .../hunter/settings/SettingsStateHolder.kt | 7 + .../hunter/settings/SettingsStrings.kt | 4 + .../hunter/settings/SettingsViewIntent.kt | 2 + .../hunter/settings/di/Module.kt | 1 + .../hunter/settings/ui/MenuScreen.kt | 7 + feature/share-content/compose/.gitignore | 1 + .../share-content/compose/build.gradle.kts | 46 +++++ .../drawable/IconContentPaste.xml | 11 ++ .../ShareContentExportMonsterFeature.kt | 79 +++++++++ .../shareContent/ShareContentImportFeature.kt | 55 ++++++ .../hunter/shareContent/di.kt | 14 ++ .../GetMonsterContentToExportUseCase.kt | 43 +++++ .../domain/ImportContentUseCase.kt | 78 +++++++++ .../mapper/DomainToShareMonsterLoreMapper.kt | 17 ++ .../mapper/DomainToShareMonsterMapper.kt | 126 ++++++++++++++ .../domain/mapper/DomainToShareSpellMapper.kt | 23 +++ .../mapper/ShareMonsterLoreToDomainMapper.kt | 18 ++ .../mapper/ShareMonsterToDomainMapper.kt | 162 ++++++++++++++++++ .../domain/mapper/ShareSpellToDomainMapper.kt | 27 +++ .../shareContent/domain/model/ShareMonster.kt | 118 +++++++++++++ .../domain/model/ShareMonsterLore.kt | 15 ++ .../shareContent/domain/model/ShareSpell.kt | 21 +++ .../shareContent/state/ShareContentState.kt | 32 ++++ .../state/ShareContentStateHolder.kt | 80 +++++++++ .../shareContent/state/ShareContentStrings.kt | 45 +++++ .../shareContent/state/ShareContentUiEvent.kt | 5 + .../ui/ShareContentExportScreen.kt | 37 ++++ .../ui/ShareContentImportScreen.kt | 86 ++++++++++ feature/share-content/event/build.gradle.kts | 13 ++ .../event/ShareContentEventDispatcher.kt | 27 +++ settings.gradle | 2 + .../hunter/ui/compose/AppTextField.kt | 3 +- .../hunter/ui/compose/BottomSheet.kt | 3 +- 89 files changed, 1476 insertions(+), 204 deletions(-) create mode 100644 app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/AppEventDispatcher.kt create mode 100644 app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/di.kt create mode 100644 core/event/src/commonMain/kotlin/br/alexandregpereira/hunter/event/v2/Event.kt create mode 100644 domain/app/data/src/commonMain/sqldelight/databases/26.db create mode 100644 domain/app/data/src/commonMain/sqldelight/migrations/25.sqm create mode 100644 domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/SaveSpellsUseCase.kt delete mode 100644 domain/spell/data/src/androidMain/AndroidManifest.xml delete mode 100644 domain/spell/data/src/androidMain/kotlin/br/alexandregpereira/hunter/data/spell/di/AndroidDataModule.kt delete mode 100644 domain/spell/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/spell/di/IosModule.kt delete mode 100644 domain/spell/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/spell/di/JvmModule.kt create mode 100644 feature/share-content/compose/.gitignore create mode 100644 feature/share-content/compose/build.gradle.kts create mode 100644 feature/share-content/compose/src/commonMain/composeResources/drawable/IconContentPaste.xml create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentExportMonsterFeature.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentImportFeature.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/di.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/GetMonsterContentToExportUseCase.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/ImportContentUseCase.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterLoreMapper.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterMapper.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareSpellMapper.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterLoreToDomainMapper.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterToDomainMapper.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareSpellToDomainMapper.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonster.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonsterLore.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareSpell.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentState.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStateHolder.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStrings.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentUiEvent.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentExportScreen.kt create mode 100644 feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentImportScreen.kt create mode 100644 feature/share-content/event/build.gradle.kts create mode 100644 feature/share-content/event/src/commonMain/kotlin/br/alexadregpereira/hunter/shareContent/event/ShareContentEventDispatcher.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd8eb3fe5..d6aaf0726 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,6 +59,7 @@ multiplatform { implementation(project(":feature:folder-list:event")) implementation(project(":feature:folder-preview:event")) implementation(project(":feature:monster-content-manager:event")) + implementation(project(":feature:share-content:event")) implementation(project(":domain:monster:event")) implementation(project(":feature:folder-detail:compose")) @@ -72,6 +73,7 @@ multiplatform { implementation(project(":feature:monster-registration:compose")) implementation(project(":feature:search:compose")) implementation(project(":feature:settings:compose")) + implementation(project(":feature:share-content:compose")) implementation(project(":feature:spell-compendium:compose")) implementation(project(":feature:spell-detail:compose")) implementation(project(":feature:sync:compose")) diff --git a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/MainViewModel.kt b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/MainViewModel.kt index b57c15c29..7894150ba 100644 --- a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/MainViewModel.kt +++ b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/MainViewModel.kt @@ -5,6 +5,7 @@ import br.alexandregpereira.hunter.app.BottomBarItemIcon.FOLDERS import br.alexandregpereira.hunter.app.BottomBarItemIcon.SEARCH import br.alexandregpereira.hunter.app.BottomBarItemIcon.SETTINGS import br.alexandregpereira.hunter.app.MainViewEvent.BottomNavigationItemClick +import br.alexandregpereira.hunter.app.event.AppEventDispatcher import br.alexandregpereira.hunter.event.folder.detail.FolderDetailResultListener import br.alexandregpereira.hunter.event.folder.detail.collectOnVisibilityChanges import br.alexandregpereira.hunter.event.folder.list.FolderListResultListener @@ -37,6 +38,7 @@ internal class MainViewModel( private val bottomBarEventManager: BottomBarEventManager, private val appLocalization: AppReactiveLocalization, private val stateRecovery: StateRecovery, + appEventDispatcher: AppEventDispatcher, ) : UiModel(MainViewState()) { init { @@ -47,6 +49,7 @@ internal class MainViewModel( observeFolderListResults() observeMonsterContentManagerEvents() observeLanguageChanges() + appEventDispatcher.observeEvents() } private fun observeLanguageChanges() { diff --git a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/di/AppModule.kt b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/di/AppModule.kt index 4978e9d58..10e447b1d 100644 --- a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/di/AppModule.kt +++ b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/di/AppModule.kt @@ -2,6 +2,7 @@ package br.alexandregpereira.hunter.app.di import br.alexandregpereira.hunter.analytics.di.analyticsModule import br.alexandregpereira.hunter.app.MainViewModel +import br.alexandregpereira.hunter.app.event.appEventModule import br.alexandregpereira.hunter.data.di.dataModules import br.alexandregpereira.hunter.detail.di.featureMonsterDetailModule import br.alexandregpereira.hunter.domain.di.domainModules @@ -19,6 +20,7 @@ import br.alexandregpereira.hunter.monster.lore.detail.di.featureMonsterLoreDeta import br.alexandregpereira.hunter.monster.registration.di.featureMonsterRegistrationModule import br.alexandregpereira.hunter.search.di.featureSearchModule import br.alexandregpereira.hunter.settings.di.featureSettingsModule +import br.alexandregpereira.hunter.shareContent.featureShareContentModule import br.alexandregpereira.hunter.spell.compendium.di.featureSpellCompendiumModule import br.alexandregpereira.hunter.spell.detail.di.featureSpellDetailModule import br.alexandregpereira.hunter.sync.di.featureSyncModule @@ -48,12 +50,14 @@ internal fun KoinApplication.initKoinModules() { featureSettingsModule, featureSpellCompendiumModule, featureSpellDetailModule, + featureShareContentModule, ) modules( analyticsModule, bottomBarEventModule, localizationModule, monsterEventModule, + appEventModule, ) } @@ -74,6 +78,7 @@ private val appModule = module { bottomBarEventManager = get(), appLocalization = get(), stateRecovery = get(named(AppStateRecoveryQualifier)), + appEventDispatcher = get(), ) } } diff --git a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/AppEventDispatcher.kt b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/AppEventDispatcher.kt new file mode 100644 index 000000000..99011b05b --- /dev/null +++ b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/AppEventDispatcher.kt @@ -0,0 +1,33 @@ +package br.alexandregpereira.hunter.app.event + +import br.alexadregpereira.hunter.shareContent.event.ShareContentEvent +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher +import br.alexandregpereira.hunter.monster.event.MonsterEvent +import br.alexandregpereira.hunter.monster.event.MonsterEventDispatcher +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +interface AppEventDispatcher { + fun observeEvents() +} + +internal class AppEventDispatcherImpl( + private val shareContentEventDispatcher: ShareContentEventDispatcher, + private val monsterEventDispatcher: MonsterEventDispatcher, +) : AppEventDispatcher { + + private val scope = MainScope() + + override fun observeEvents() { + observeShareContentEvents() + } + + private fun observeShareContentEvents() { + shareContentEventDispatcher.events.filterIsInstance() + .onEach { + monsterEventDispatcher.dispatchEvent(MonsterEvent.OnCompendiumChanges) + }.launchIn(scope) + } +} diff --git a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/di.kt b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/di.kt new file mode 100644 index 000000000..aeaab6608 --- /dev/null +++ b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/event/di.kt @@ -0,0 +1,7 @@ +package br.alexandregpereira.hunter.app.event + +import org.koin.dsl.module + +val appEventModule = module { + single { AppEventDispatcherImpl(get(), get()) } +} diff --git a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppMainScreen.kt b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppMainScreen.kt index 3e334523a..2f5e80a2f 100644 --- a/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppMainScreen.kt +++ b/app/src/commonMain/kotlin/br/alexandregpereira/hunter/app/ui/AppMainScreen.kt @@ -16,6 +16,8 @@ import br.alexandregpereira.hunter.folder.preview.FolderPreviewFeature import br.alexandregpereira.hunter.monster.content.MonsterContentManagerFeature import br.alexandregpereira.hunter.monster.lore.detail.MonsterLoreDetailFeature import br.alexandregpereira.hunter.monster.registration.MonsterRegistrationFeature +import br.alexandregpereira.hunter.shareContent.ShareContentExportMonsterFeature +import br.alexandregpereira.hunter.shareContent.ShareContentImportFeature import br.alexandregpereira.hunter.spell.compendium.SpellCompendiumFeature import br.alexandregpereira.hunter.spell.detail.SpellDetailFeature import br.alexandregpereira.hunter.sync.SyncFeature @@ -80,6 +82,10 @@ internal fun AppMainScreen( FolderInsertFeature(contentPadding = contentPadding) + ShareContentExportMonsterFeature(contentPadding = contentPadding) + + ShareContentImportFeature(contentPadding = contentPadding) + SyncFeature() } } diff --git a/core/event/src/commonMain/kotlin/br/alexandregpereira/hunter/event/v2/Event.kt b/core/event/src/commonMain/kotlin/br/alexandregpereira/hunter/event/v2/Event.kt new file mode 100644 index 000000000..a804e9104 --- /dev/null +++ b/core/event/src/commonMain/kotlin/br/alexandregpereira/hunter/event/v2/Event.kt @@ -0,0 +1,30 @@ +package br.alexandregpereira.hunter.event.v2 + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow + +interface EventDispatcher : EventListener { + + fun dispatchEvent(event: Event) +} + +interface EventListener { + + val events: Flow +} + +fun EventDispatcher(): EventDispatcher = DefaultEventManager() + +private class DefaultEventManager : EventDispatcher { + + private val _events: MutableSharedFlow = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + override val events: Flow = _events + + override fun dispatchEvent(event: Event) { + _events.tryEmit(event) + } +} 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 fd6db66f8..013e0c2a9 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 @@ -119,6 +119,7 @@ internal fun MonsterEntity.toDatabaseEntity(): MonsterDatabaseEntity { MonsterEntityStatus.Original -> 0L MonsterEntityStatus.Clone -> 1L MonsterEntityStatus.Edited -> 2L + MonsterEntityStatus.Imported -> 3L } ) } diff --git a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/SpellDaoImpl.kt b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/SpellDaoImpl.kt index bee004050..672a90ec8 100644 --- a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/SpellDaoImpl.kt +++ b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/database/dao/SpellDaoImpl.kt @@ -46,7 +46,8 @@ internal class SpellDaoImpl( damageType = it.damageType, school = it.school, description = it.description, - higherLevel = it.higherLevel + higherLevel = it.higherLevel, + status = it.status.toLong(), ) ) } @@ -86,7 +87,8 @@ internal class SpellDaoImpl( damageType = damageType, school = school, description = description, - higherLevel = higherLevel + higherLevel = higherLevel, + status = status.toInt(), ) } } 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 319de05c2..2ac97a51e 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 @@ -25,4 +25,4 @@ getMonsters: SELECT * FROM MonsterEntity; getMonstersEdited: -SELECT * FROM MonsterEntity WHERE isClone == 2; +SELECT * FROM MonsterEntity WHERE isClone > 0; diff --git a/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Spell.sq b/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Spell.sq index 1ae95352c..056410a2b 100644 --- a/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Spell.sq +++ b/domain/app/data/src/commonMain/sqldelight/br/alexandregpereira/hunter/database/Spell.sq @@ -1,10 +1,10 @@ -CREATE TABLE IF NOT EXISTS SpellEntity (`spellIndex` TEXT NOT NULL, `name` TEXT NOT NULL, `level` INTEGER NOT NULL, `castingTime` TEXT NOT NULL, `components` TEXT NOT NULL, `duration` TEXT NOT NULL, `range` TEXT NOT NULL, `ritual` INTEGER NOT NULL, `concentration` INTEGER NOT NULL, `savingThrowType` TEXT, `damageType` TEXT, `school` TEXT NOT NULL, `description` TEXT NOT NULL, `higherLevel` TEXT, PRIMARY KEY(`spellIndex`)); +CREATE TABLE IF NOT EXISTS SpellEntity (`spellIndex` TEXT NOT NULL, `name` TEXT NOT NULL, `level` INTEGER NOT NULL, `castingTime` TEXT NOT NULL, `components` TEXT NOT NULL, `duration` TEXT NOT NULL, `range` TEXT NOT NULL, `ritual` INTEGER NOT NULL, `concentration` INTEGER NOT NULL, `savingThrowType` TEXT, `damageType` TEXT, `school` TEXT NOT NULL, `description` TEXT NOT NULL, `higherLevel` TEXT, `status` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`spellIndex`)); insert: INSERT OR REPLACE INTO SpellEntity VALUES ?; deleteAll: -DELETE FROM SpellEntity; +DELETE FROM SpellEntity WHERE status == 0; getSpell: SELECT * FROM SpellEntity WHERE spellIndex == ?; diff --git a/domain/app/data/src/commonMain/sqldelight/databases/26.db b/domain/app/data/src/commonMain/sqldelight/databases/26.db new file mode 100644 index 0000000000000000000000000000000000000000..49bf44958b18d86c95643cd7ba092009aef25560 GIT binary patch literal 303104 zcmeI(Pi!35eZcWuO4^~8q9p$v+j5dg6h#_Kv`j0sA~TAzqE^hBeHHa z=ae6h*9vbduOFE$OdM7Y|L52r4*hff_hWyT`)&Rgxu1*w#Glg31$FIWK4-NX=KI~p zO;PD}W6QDq@9V3^w${jp@kC9F!Mk;n{+^ekIFCUibBBdHQqi(o=!ijBmCS zyv~bu$K&ek1TiL0eL7|)FOv~lVsvZv29v!)DmGdqhpO0%C4yMj7oHx5Bd_H8v^@P8 z_udIbou5ySE&fY5*Ri|Z74uzDIKLNOu<<;Zoq ze*a`aZOv!JkY6qTQvXxDq0u2olKqr>ds0!Sr}MiDg8}(=$K1Be-n8al6iYdt=85Hp zCh}I-e^bY zvwQQP-_3py#pB0M-=j=1$aYYy+*^}__nLXY4gFA&UPxvzF`bM#!ez-P7YtQ=j zLS&CtO}K9=!RqGhAZYFy&8@-e#%x5sk0;$?xxz@kwA=RXc++1!JEmkQAYKNuvDdoB zzjZ+67ZyvK@jaF{vD)y?$i-Qot@3QkNwt{nqsBdHXLi)T(!44;f$Vj@ z6w=NWHSwor)H`;2r_~!1BEc{iH-duoj%f&^l9PVviuzKapc=DTVUbY=QIF)^yv8e1LF zWwO;`$7pXzwiF%9*)rrKcik52II%A2-LSVR9rTfyW@&A)s=anWi-c5fj}i2XuFo9aW8@8cx;JD5 zy?excEAI!#?mIznylaK!RtF!);=wV<@TL>#mQC!=8rDNAV^WdNw4Y+C@t-NkVwVVm zv7rWodJBX87qOxK((B@%FTOPCZLUvx|L_L^1Q0*~0R#|0009ILKmY**MpdBbHH`iL zsCF;yLI42-5I_I{1Q0*~0R#|0z!%{BpFltW0R#|0009ILKmY**5I|t`1vvj7{XV9J z2q1s}0tg_000IagfB*sraQ;sWAbv=9LV5I_I{1Q0*~0R#|000GYb zi2(!0fb;*+?_*ks00IagfB*srAbM zl>2S&_s75=91_K{DH%pR^DRt~)} z_J^^n`QPVX5f}Yj{3rgDUM{F>7xOu*-7w$pK5kl$xo&JZw*P&7)!4S$8`aH@{a(54 zSkBJ+bSmi!?yVOU_5At#?wVsfXqt0&yJ0z&-4<5_eI#n}AD{6VS6?cqcg|;3Gx|us zl8J;)}JGSjJT zqa{gg+3l`lb}Alv>YCdtm8H_keQlw9KYWQT!|Zok zBIQN*s;a0{Q~BK~zt@siB)`89LrLZem2Uq5l}4&bwT+Iw^(dX}UsN}`j)*eJWig4^ z19biJ^96O`Ocp??-its#9HQS;clkL*J#!|%yY4qUniu{2e5}FI94`bUO*9r z5c2fr+{QDCdgV%fx9;o6OUYB8jfu(29F*5*;pt&!OTp{8Xm>oW&QACe$4m!1mD zW_&YwDS7JCF*A9YjCd2nakDqH>=kCMCm(LSfVzLOptk0-0&4K?`=91IcDK7?J`9b9 zW`k419b+lS(`|_5hZUDsttKBln&!6I><>WE394J)v~7`pWzuzyD(c!=th0O5m`~sbN&L<5)AuM-Ofoc^dt4EQNcJ2$ zd}2;_{@_kwgohEY>l?*_`r+CTtv&183z0oqHQ~Og1dEfigP^%2B49!s-W?RtR~sqayD>WHGw z&gR|6e(Ot3u~cjujWvAJU>J-WLBV>* zG=x#fNxyVOeW_4TjoGZQ$f&~lbVi2qas7}#V*F?_?EGf4KVpO*567D6zg$6?@QwJK z2F+fiRxQ~wzHYZ3*=@7!h!tOARIN3(I-<*Di@uK0-jM7GI+n9#$VcwFEf#KKA=0~H zZ*e*3BQX^XHfq)WKEzuV_^ZEIQpPkee4)B&ZETvI#b9pnn(9ToV>pquZofL(jq=UX z+G16E?Sd8wsooYLoLpuO?=kX*J>45Jg5EvizLocbWA~jPINr6wa;t-nWAWe^WO&nw zbk8NWR}Jf-l`*NvXWCCO)%ed8WU)(heP(Q^!Jyv4VDROT*j0~vJL}1#zsbFjdz>5p z-|@dy{(OA9@QyNn=&uUe*!@FB{^HolWA7ikG;wd@WKlcu*T;Wz{4bB^CV!E8eDvQY zjumePJ9Fv3#g~KIkDbi@Qk|K}?_LexLw9ysQFT4PJME{em_}rq76112SQo_eL@&ri zhwhbPcFET*7H9s=)7tL$&nSL-f{kCWL8-J_TWzsP48lv6b}rTuS&8hZlFvKx#y@%c zf)GwOaQIF4h|2 ze#v38C}t#Y&`eLd;vTXWw5*V zw)i2`o82W-uE_OzI$hbW+3uQ>6+u(X`&**!lB}-3u3MJ=R%mtSnqrZWu0!TL8@wsH ze&s6#b?I!@cn}M+_37BxGp_X|MZJ7EzZ)z;7tIZ`-7q>k(ci(OiC>8Yvb2a7(2~{m z|J(0p6|JCt?{XHCvw@rB?=fR;Z&mL6E*2lbuRdhkP%_?S-7VqlW|p}UuK zREYOr=vt=r#J>gkqlkaI$dA?HSAj1M`&A&_0I|?dF?{*+ICl8%DMh__F~7SJEKde2 z;9y8i9aOv=lTKx4kF!HP<>Td(D`!pSli$r1?wp@}y`bLGvtl#8tXiLrf1hz%=N0wZ zwfwH-3kCDl-K}QZ>=;3p1X7t9ug1hOi)G6VCZg!Lx?g1FR|@L=OIcXG0q&;VY?z&( z;r&rBnmFa&*A?~BrTp%FpG|U|BadE20iqjEgR;CuT6@zA^YzO#n|FE@{H8G zTh!;y4SRW`zfpxk>9q5%aW*>2rAwq!UWxIf^F;`Cz26Q8zdofF3i4R`oVE;!hwHl7 z85tRenqnAF918w1Cx)fhh8?{20;qQaA)eR|)j`rF_o<8i2L#zpdLfl9 zI-xZ<`KZX&+wl#3sa@Ew73|jfyU$b^+1*}?HBnYV zv{F6*eNLg|-#qo|>31#^RP$Qa;FLMGhMNk>uzivL|G)J23lb4P009ILKmY**5I_I{ z1P~Z00sjC0Bh{TW1_1;RKmY**5I_I{1Q0*~0jU7z|56qb5kLR|1Q0*~0R#|0009IL z7%2hH|3|7jX$%4gAbBqD$S0tg_000IagfB*srATUw_ zod1thchVRH5I_I{1Q0*~0R#|0009J~0-XO#Sx7_x0R#|0009ILKmY**5I|t01UUa6 zsqUmP2q1s}0tg_000IagfB*srNCi0mm$HzE00IagfB*srAbN`UkKk?Kwwg8%{uAba;00IagfB*srAbf``dFXLS*d+4g7E}Y4`rz3SM)g8-j_v+QM z+g-=(RJ^K3Gp=(R;Cb zuX)x5Z^r5zh27)AU=Zm&X&}7ERUveEo$j_SE9yt0rO*6PEsYOidp!KgXKQQ!S)aPK zgDZ9S@Eanw!~mU&t-D5ZD;#*bUQqcRXf`70--tCJ5@3-sUcY$P_F_JEy+0&-nfgaF zk_dm-YBpsNK1jbTtH2;T9pQt#WQ2e8b>+GF`SFkQ+m`uWWOSLcTaTKi)04QnS7xt5 zcPr(4+R(dbrPbnUd9gfK)n3tVuH0IR77tootSm2=SG2coRhAQ(;TT&Kb4%nY;<7O3 z)mCKg(yk^-iAgss;Emc9Z-R3lUs9f1SQy{^UPR!g-E5ehOoOa!i9%M->z!Q?!zs%Dn@j-e&4SNPpf9vgajrO_z$@q`w4-JttYa(>Kz+A2hAfYHhXUy)P?pQ&K}&xl|oxg_5;Y z=EOQ)OkvS=d~n?jtG{**Ud~D*nNiNHndD+mEqMd&TzG5UJ$qeIzjrzB&PU>K(cCcG z4WqMDiVh~xS6Eu&hNNE7f+;d-sifJdQabK>FZ60XSjc-jvuHKb*>jRQrc1?c;t8gU zBx|RBTT$08=H1H?$5mrnY!9oO9s51mOOedIE~(q_TllX;@;1quow%l`ch2YCi;JBP{ z00IagfB*srAbIM0tg_0 z00IagfB*srAh0h2?Em+Lm75}f00IagfB*srAbV|9xTQrU)Q_00IagfB*srAb8{2x7?*cbo+ literal 0 HcmV?d00001 diff --git a/domain/app/data/src/commonMain/sqldelight/migrations/25.sqm b/domain/app/data/src/commonMain/sqldelight/migrations/25.sqm new file mode 100644 index 000000000..31b7c9cd4 --- /dev/null +++ b/domain/app/data/src/commonMain/sqldelight/migrations/25.sqm @@ -0,0 +1 @@ +ALTER TABLE SpellEntity ADD COLUMN status INTEGER NOT NULL DEFAULT 0; diff --git a/domain/app/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/di/JvmSqlDriverFactory.kt b/domain/app/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/di/JvmSqlDriverFactory.kt index e5963f667..54e09ac55 100644 --- a/domain/app/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/di/JvmSqlDriverFactory.kt +++ b/domain/app/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/di/JvmSqlDriverFactory.kt @@ -23,13 +23,49 @@ import org.koin.core.scope.Scope import java.io.File internal actual fun Scope.createSqlDriver(): SqlDriver { + val databaseFile = getDatabaseFile() + val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:${databaseFile.absolutePath}") + + if (databaseFile.exists()) { + val currentVersion = driver.getDatabaseVersion() + val schemaVersion = Database.Schema.version + if (schemaVersion > currentVersion) { + Database.Schema.migrate(driver, currentVersion, schemaVersion) + driver.setDatabaseVersion(schemaVersion) + println("init: migrated from $currentVersion to $schemaVersion") + } else { + Database.Schema.create(driver) + } + } else { + driver.setDatabaseVersion(Database.Schema.version) + Database.Schema.create(driver) + } + + return driver +} + +private fun getDatabaseFile(): File { val userFolder = System.getProperty("user.home") val appDataFolder = File(userFolder, ".monster-compendium") if (appDataFolder.exists().not()) { appDataFolder.mkdirs() } - val databasePath = File(appDataFolder, "hunter-database.db") - val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:${databasePath.absolutePath}") - Database.Schema.create(driver) - return driver + return File(appDataFolder, "hunter-database.db") +} + +private fun SqlDriver.getDatabaseVersion(): Int { + val sqlCursor = this.executeQuery( + identifier = null, + sql = "PRAGMA user_version;", + parameters = 0, + binders = null + ) + + val initialDatabaseVersion = 25 + return sqlCursor.getLong(0)?.toInt() + ?.takeUnless { it == 0 } ?: initialDatabaseVersion +} + +private fun SqlDriver.setDatabaseVersion(version: Int) { + this.execute(null, "PRAGMA user_version = $version;", 0, null) } diff --git a/domain/monster-lore/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/lore/local/dao/MonsterLoreDao.kt b/domain/monster-lore/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/lore/local/dao/MonsterLoreDao.kt index e3c97b016..8b1056c87 100644 --- a/domain/monster-lore/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/lore/local/dao/MonsterLoreDao.kt +++ b/domain/monster-lore/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/monster/lore/local/dao/MonsterLoreDao.kt @@ -17,8 +17,6 @@ package br.alexandregpereira.hunter.data.monster.lore.local.dao import br.alexandregpereira.hunter.data.monster.lore.local.entity.MonsterLoreCompleteEntity -import br.alexandregpereira.hunter.data.monster.lore.local.entity.MonsterLoreEntity -import br.alexandregpereira.hunter.data.monster.lore.local.entity.MonsterLoreEntryEntity interface MonsterLoreDao { 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 9957d9b0a..3ca3820e7 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 @@ -16,9 +16,6 @@ package br.alexandregpereira.hunter.domain.model -import kotlin.native.ObjCName - -@ObjCName(name = "AbilityDescription", exact = true) data class AbilityDescription( val name: String, val description: String, diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityScore.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityScore.kt index e723fa9b3..a07d0b49e 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityScore.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/AbilityScore.kt @@ -16,16 +16,12 @@ package br.alexandregpereira.hunter.domain.model -import kotlin.native.ObjCName - -@ObjCName(name = "AbilityScore", exact = true) data class AbilityScore( val type: AbilityScoreType, val value: Int, val modifier: Int ) -@ObjCName(name = "AbilityScoreType", exact = true) enum class AbilityScoreType { STRENGTH, DEXTERITY, 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 dce924f2f..9288122a4 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "Action", exact = true) data class Action( val id: String, val damageDices: List, @@ -27,7 +25,6 @@ data class Action( val abilityDescription: AbilityDescription ) -@ObjCName(name = "DamageDice", exact = true) data class DamageDice( val dice: String, val 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 df49ce1d3..884c7ffd7 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "Condition", exact = true) data class Condition( val index: String, val type: ConditionType, @@ -39,7 +37,6 @@ data class Condition( } } -@ObjCName(name = "ConditionType", exact = true) enum class ConditionType { BLINDED, CHARMED, 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 a267beb0a..81f698a2d 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 @@ -16,16 +16,12 @@ package br.alexandregpereira.hunter.domain.model -import kotlin.native.ObjCName - -@ObjCName(name = "Damage", exact = true) data class Damage( val index: String, val type: DamageType, val name: String ) -@ObjCName(name = "DamageType", exact = true) enum class DamageType { ACID, BLUDGEONING, 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 80542350d..19a9e8c30 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 @@ -57,7 +57,7 @@ data class Monster( } enum class MonsterStatus { - Original, Edited, Clone + Original, Edited, Clone, Imported } private fun Float.getChallengeRatingFormatted(): String { diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/MonsterType.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/MonsterType.kt index 80b657fe1..cfe99b751 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/MonsterType.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/MonsterType.kt @@ -16,9 +16,6 @@ package br.alexandregpereira.hunter.domain.model -import kotlin.native.ObjCName - -@ObjCName(name = "MonsterType", exact = true) enum class MonsterType { ABERRATION, BEAST, 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 42431ab63..07ef1e855 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "SavingThrow", exact = true) data class SavingThrow( val index: String, val modifier: Int, 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 1a5a5699a..5094d509a 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "Skill", exact = true) data class Skill( val index: String, val modifier: Int, 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 ea379375c..074ccaaa5 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 @@ -16,23 +16,17 @@ package br.alexandregpereira.hunter.domain.model -import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName - -@ObjCName(name = "Speed", exact = true) data class Speed( val hover: Boolean, val values: List, ) -@ObjCName(name = "SpeedValue", exact = true) data class SpeedValue( val type: SpeedType, val valueFormatted: String, val index: String = "", ) -@ObjCName(name = "SpeedType", exact = true) enum class SpeedType { BURROW, CLIMB, diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Stats.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Stats.kt index 4d0504f2b..b069ba1e1 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Stats.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Stats.kt @@ -16,9 +16,6 @@ package br.alexandregpereira.hunter.domain.model -import kotlin.native.ObjCName - -@ObjCName(name = "Stats", exact = true) data class Stats( val armorClass: Int = 0, val hitPoints: Int = 0, 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 c6389dcc4..feb43e39f 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.monster.spell.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "SpellPreview", exact = true) data class SpellPreview( val index: String, val name: String, @@ -42,7 +40,6 @@ data class SpellPreview( } } -@ObjCName(name = "SchoolOfMagic", exact = true) enum class SchoolOfMagic { ABJURATION, CONJURATION, 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 39d0d6ac6..fb26861aa 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.monster.spell.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "SpellUsage", exact = true) data class SpellUsage( val group: String, val spells: List, 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 dc003875f..90fab3aa1 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 @@ -17,9 +17,7 @@ package br.alexandregpereira.hunter.domain.monster.spell.model import br.alexandregpereira.hunter.uuid.generateUUID -import kotlin.native.ObjCName -@ObjCName(name = "Spellcasting", exact = true) data class Spellcasting( val description: String, val type: SpellcastingType, diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellcastingType.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellcastingType.kt index b00847788..9431c1d72 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellcastingType.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/monster/spell/model/SpellcastingType.kt @@ -16,9 +16,7 @@ package br.alexandregpereira.hunter.domain.monster.spell.model -import kotlin.native.ObjCName -@ObjCName(name = "SpellcastingType", exact = true) enum class SpellcastingType { SPELLCASTER, INNATE } diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SaveMonstersUseCase.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SaveMonstersUseCase.kt index 44242f4f5..d87aadc73 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SaveMonstersUseCase.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/usecase/SaveMonstersUseCase.kt @@ -28,14 +28,28 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.zip -class SaveMonstersUseCase internal constructor( +interface SaveMonstersUseCase { + operator fun invoke(monsters: List, isSync: Boolean = false): Flow +} + +fun SaveMonstersUseCase( + getMeasurementUnitUseCase: GetMeasurementUnitUseCase, + monsterRepository: MonsterRepository, + measurementUnitRepository: MeasurementUnitRepository, +): SaveMonstersUseCase = SaveMonstersUseCaseImpl( + getMeasurementUnitUseCase, + monsterRepository, + measurementUnitRepository +) + +private class SaveMonstersUseCaseImpl( private val getMeasurementUnitUseCase: GetMeasurementUnitUseCase, private val monsterRepository: MonsterRepository, private val measurementUnitRepository: MeasurementUnitRepository, -) { +) : SaveMonstersUseCase { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(monsters: List, isSync: Boolean = false): Flow { + override operator fun invoke(monsters: List, isSync: Boolean): Flow { return getMeasurementUnitUseCase() .zip(measurementUnitRepository.getPreviousMeasurementUnit()) { unit, previousUnit -> monsters.changeMonstersMeasurementUnit(previousUnit, unit) 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 344fd75a9..9cf4a8850 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 @@ -59,7 +59,7 @@ class SyncMonstersUseCase internal constructor( } .reduce { accumulator, value -> accumulator + value } } - .filterMonstersNotEdited() + .filterModifiedMonsters() .flatMapLatest { monsters -> saveMonstersUseCase(monsters = monsters, isSync = true) }.flatMapLatest { @@ -92,7 +92,7 @@ class SyncMonstersUseCase internal constructor( it.appendMonsterImages(monsterImages) } - private fun Flow>.filterMonstersNotEdited(): Flow> = map { monsters -> + private fun Flow>.filterModifiedMonsters(): Flow> = map { monsters -> val monstersEditedIndexes = localRepository.getMonsterPreviewsEdited().single() .map { it.index } .toSet() 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 08d0dcef3..e529568f6 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 @@ -40,5 +40,5 @@ data class MonsterEntity( ) enum class MonsterEntityStatus { - Original, Clone, Edited + Original, Clone, Edited, Imported } 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 f3b65a078..25279ce46 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 @@ -86,6 +86,7 @@ internal fun Monster.toEntity(): MonsterCompleteEntity { MonsterStatus.Original -> MonsterEntityStatus.Original MonsterStatus.Clone -> MonsterEntityStatus.Clone MonsterStatus.Edited -> MonsterEntityStatus.Edited + MonsterStatus.Imported -> MonsterEntityStatus.Imported }, ), speed = speed.toEntity(index), @@ -140,6 +141,7 @@ private fun MonsterEntity.toDomain(): Monster { MonsterEntityStatus.Original -> MonsterStatus.Original MonsterEntityStatus.Clone -> MonsterStatus.Clone MonsterEntityStatus.Edited -> MonsterStatus.Edited + MonsterEntityStatus.Imported -> MonsterStatus.Imported } ) } diff --git a/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/SaveSpellsUseCase.kt b/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/SaveSpellsUseCase.kt new file mode 100644 index 000000000..13bc01323 --- /dev/null +++ b/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/SaveSpellsUseCase.kt @@ -0,0 +1,14 @@ +package br.alexandregpereira.hunter.domain.spell + +import br.alexandregpereira.hunter.domain.spell.model.Spell +import kotlinx.coroutines.flow.Flow + +fun interface SaveSpells { + operator fun invoke(spells: List): Flow +} + +internal fun SaveSpells( + spellRepository: SpellLocalRepository, +): SaveSpells = SaveSpells { spells -> + spellRepository.saveSpells(spells) +} diff --git a/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/di/DomainModule.kt b/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/di/DomainModule.kt index 93081ab1d..4f9c8a178 100644 --- a/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/di/DomainModule.kt +++ b/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/di/DomainModule.kt @@ -18,6 +18,7 @@ package br.alexandregpereira.hunter.domain.spell.di import br.alexandregpereira.hunter.domain.spell.GetSpellUseCase import br.alexandregpereira.hunter.domain.spell.GetSpellsByIdsUseCase +import br.alexandregpereira.hunter.domain.spell.SaveSpells import br.alexandregpereira.hunter.domain.spell.SyncSpellsUseCase import org.koin.dsl.module @@ -25,4 +26,5 @@ val spellDomainModule = module { factory { GetSpellsByIdsUseCase(get()) } factory { GetSpellUseCase(get()) } factory { SyncSpellsUseCase(get(), get()) } + factory { SaveSpells(get()) } } diff --git a/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/model/Spell.kt b/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/model/Spell.kt index 529671377..38162e186 100644 --- a/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/model/Spell.kt +++ b/domain/spell/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/spell/model/Spell.kt @@ -30,5 +30,11 @@ data class Spell( val damageType: String?, val school: SchoolOfMagic, val description: String, - val higherLevel: String? + val higherLevel: String?, + val status: SpellStatus, ) + +enum class SpellStatus { + Original, + Imported +} diff --git a/domain/spell/data/src/androidMain/AndroidManifest.xml b/domain/spell/data/src/androidMain/AndroidManifest.xml deleted file mode 100644 index c63dbe024..000000000 --- a/domain/spell/data/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/domain/spell/data/src/androidMain/kotlin/br/alexandregpereira/hunter/data/spell/di/AndroidDataModule.kt b/domain/spell/data/src/androidMain/kotlin/br/alexandregpereira/hunter/data/spell/di/AndroidDataModule.kt deleted file mode 100644 index 0d1ba5847..000000000 --- a/domain/spell/data/src/androidMain/kotlin/br/alexandregpereira/hunter/data/spell/di/AndroidDataModule.kt +++ /dev/null @@ -1,30 +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.data.spell.di - -import br.alexandregpereira.hunter.domain.spell.SpellLocalRepository -import org.koin.core.module.Module -import org.koin.core.scope.Scope -import org.koin.dsl.module - -internal actual fun getAdditionalModule(): Module { - return module { } -} - -internal actual fun Scope.createLocalRepository(): SpellLocalRepository? { - return null -} diff --git a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/di/DataModule.kt b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/di/DataModule.kt index e3839b994..af8f054f4 100644 --- a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/di/DataModule.kt +++ b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/di/DataModule.kt @@ -28,19 +28,13 @@ import br.alexandregpereira.hunter.domain.spell.SpellLocalRepository import br.alexandregpereira.hunter.domain.spell.SpellRemoteRepository import br.alexandregpereira.hunter.domain.spell.SpellRepository import br.alexandregpereira.hunter.domain.spell.SpellSettingsRepository -import org.koin.core.module.Module -import org.koin.core.scope.Scope import org.koin.dsl.module val spellDataModule = module { factory { DefaultSpellLocalDataSource(get()) } factory { DefaultSpellRepository(get(), get()) } - factory { createLocalRepository() ?: DefaultSpellLocalRepository(get()) } + factory { DefaultSpellLocalRepository(get()) } factory { DefaultSpellRemoteRepository(get()) } factory { DefaultSpellRemoteDataSource(get(), get()) } factory { SpellSettingsRepositoryImpl(get()) } -}.apply { includes(getAdditionalModule()) } - -internal expect fun getAdditionalModule(): Module - -internal expect fun Scope.createLocalRepository(): SpellLocalRepository? +} diff --git a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/mapper/SpellEntityMapper.kt b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/mapper/SpellEntityMapper.kt index 92cd3a761..7c71cd13c 100644 --- a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/mapper/SpellEntityMapper.kt +++ b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/mapper/SpellEntityMapper.kt @@ -20,6 +20,7 @@ import br.alexandregpereira.hunter.data.spell.local.model.SpellEntity import br.alexandregpereira.hunter.domain.spell.model.SavingThrowType import br.alexandregpereira.hunter.domain.spell.model.SchoolOfMagic import br.alexandregpereira.hunter.domain.spell.model.Spell +import br.alexandregpereira.hunter.domain.spell.model.SpellStatus internal fun SpellEntity.toDomain(): Spell { return Spell( @@ -36,7 +37,11 @@ internal fun SpellEntity.toDomain(): Spell { damageType = damageType, school = SchoolOfMagic.valueOf(school), description = description, - higherLevel = higherLevel + higherLevel = higherLevel, + status = when (status) { + 0 -> SpellStatus.Original + else -> SpellStatus.Imported + }, ) } @@ -56,7 +61,11 @@ fun List.toEntity(): List { damageType = it.damageType, school = it.school.name, description = it.description, - higherLevel = it.higherLevel + higherLevel = it.higherLevel, + status = when (it.status) { + SpellStatus.Original -> 0 + SpellStatus.Imported -> 1 + }, ) } } diff --git a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/model/SpellEntity.kt b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/model/SpellEntity.kt index c94226b57..ca31a1e63 100644 --- a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/model/SpellEntity.kt +++ b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/local/model/SpellEntity.kt @@ -30,5 +30,6 @@ data class SpellEntity( val damageType: String?, val school: String, val description: String, - val higherLevel: String? + val higherLevel: String?, + val status: Int, ) diff --git a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/remote/mapper/SpellDtoMapper.kt b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/remote/mapper/SpellDtoMapper.kt index 55565749e..f6ac44b4f 100644 --- a/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/remote/mapper/SpellDtoMapper.kt +++ b/domain/spell/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/spell/remote/mapper/SpellDtoMapper.kt @@ -20,6 +20,7 @@ import br.alexandregpereira.hunter.data.spell.remote.model.SpellDto import br.alexandregpereira.hunter.domain.spell.model.SavingThrowType import br.alexandregpereira.hunter.domain.spell.model.SchoolOfMagic import br.alexandregpereira.hunter.domain.spell.model.Spell +import br.alexandregpereira.hunter.domain.spell.model.SpellStatus internal fun List.toDomain(): List { return map { @@ -37,7 +38,8 @@ internal fun List.toDomain(): List { damageType = it.damageType, school = SchoolOfMagic.valueOf(it.school.name), description = it.description, - higherLevel = it.higherLevel + higherLevel = it.higherLevel, + status = SpellStatus.Original, ) } } diff --git a/domain/spell/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/spell/di/IosModule.kt b/domain/spell/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/spell/di/IosModule.kt deleted file mode 100644 index a0b1650b1..000000000 --- a/domain/spell/data/src/iosMain/kotlin/br/alexandregpereira/hunter/data/spell/di/IosModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2023 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.data.spell.di - -import br.alexandregpereira.hunter.domain.spell.SpellLocalRepository -import org.koin.core.module.Module -import org.koin.core.scope.Scope -import org.koin.dsl.module - -internal actual fun getAdditionalModule(): Module { - return module { } -} - -internal actual fun Scope.createLocalRepository(): SpellLocalRepository? { - return null -} diff --git a/domain/spell/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/spell/di/JvmModule.kt b/domain/spell/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/spell/di/JvmModule.kt deleted file mode 100644 index a0b1650b1..000000000 --- a/domain/spell/data/src/jvmMain/kotlin/br/alexandregpereira/hunter/data/spell/di/JvmModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2023 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.data.spell.di - -import br.alexandregpereira.hunter.domain.spell.SpellLocalRepository -import org.koin.core.module.Module -import org.koin.core.scope.Scope -import org.koin.dsl.module - -internal actual fun getAdditionalModule(): Module { - return module { } -} - -internal actual fun Scope.createLocalRepository(): SpellLocalRepository? { - return null -} diff --git a/feature/folder-insert/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/insert/ui/FolderInsertScreen.kt b/feature/folder-insert/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/insert/ui/FolderInsertScreen.kt index d78b952a8..a83ba4a94 100644 --- a/feature/folder-insert/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/insert/ui/FolderInsertScreen.kt +++ b/feature/folder-insert/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/insert/ui/FolderInsertScreen.kt @@ -27,7 +27,6 @@ import br.alexandregpereira.hunter.ui.compose.AppButton import br.alexandregpereira.hunter.ui.compose.AppTextField import br.alexandregpereira.hunter.ui.compose.BottomSheet import br.alexandregpereira.hunter.ui.compose.ScreenHeader -import br.alexandregpereira.hunter.ui.theme.HunterTheme import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @@ -39,7 +38,7 @@ internal fun FolderInsertScreen( onLongClick: (String) -> Unit = {}, onSave: () -> Unit = {}, onClose: () -> Unit = {}, -) = HunterTheme { +) { BottomSheet(opened = state.isOpen, contentPadding = contentPadding, onClose = onClose) { ScreenHeader( title = state.strings.addToFolder, 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 f0800f6ce..3eab4aced 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 @@ -91,7 +91,8 @@ fun MonsterDetailFeature( options = viewState.options, showOptions = viewState.showOptions, onOptionSelected = viewModel::onOptionClicked, - onClosed = viewModel::onShowOptionsClosed + onClosed = viewModel::onShowOptionsClosed, + contentPadding = contentPadding, ) FormBottomSheet( diff --git a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailOptionPicker.kt b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailOptionPicker.kt index d5488360a..e5ebaab95 100644 --- a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailOptionPicker.kt +++ b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailOptionPicker.kt @@ -38,18 +38,20 @@ import br.alexandregpereira.hunter.ui.compose.animatePressed internal fun MonsterDetailOptionPicker( options: List, showOptions: Boolean, + contentPadding: PaddingValues = PaddingValues(), onOptionSelected: (MonsterDetailOptionState) -> Unit = {}, onClosed: () -> Unit = {} ) = BottomSheet( opened = showOptions, onClose = onClosed, ) { - MonsterDetailOptions(options, onOptionSelected) + MonsterDetailOptions(options, contentPadding, onOptionSelected) } @Composable private fun MonsterDetailOptions( options: List, + contentPadding: PaddingValues = PaddingValues(), onOptionSelected: (MonsterDetailOptionState) -> Unit = {}, ) { Column { @@ -78,7 +80,7 @@ private fun MonsterDetailOptions( Spacer( modifier = Modifier - .height(PaddingValues(all = 32.dp).calculateBottomPadding()) + .height(contentPadding.calculateBottomPadding() + 16.dp) .fillMaxWidth() ) } diff --git a/feature/monster-detail/state-holder/build.gradle.kts b/feature/monster-detail/state-holder/build.gradle.kts index e520e5dfb..86acef67e 100644 --- a/feature/monster-detail/state-holder/build.gradle.kts +++ b/feature/monster-detail/state-holder/build.gradle.kts @@ -29,6 +29,7 @@ multiplatform { implementation(project(":domain:spell:core")) implementation(project(":feature:folder-insert:event")) implementation(project(":domain:monster:event")) + implementation(project(":feature:share-content:event")) implementation(project(":feature:monster-lore-detail:event")) implementation(project(":feature:monster-registration:event")) implementation(project(":feature:spell-detail:event")) 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 cbe0c3eb5..bb3c7266d 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 @@ -151,4 +151,13 @@ class MonsterDetailAnalytics( ) ) } + + fun trackMonsterDetailExportClicked(monsterIndex: String) { + analytics.track( + eventName = "MonsterDetail - export clicked", + 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 87bde29ef..123e80600 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 @@ -16,22 +16,30 @@ package br.alexandregpereira.hunter.monster.detail +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.ADD_TO_FOLDER +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.CHANGE_TO_FEET +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.CHANGE_TO_METERS +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.CLONE +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.DELETE +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.EDIT +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.EXPORT +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.RESET_TO_ORIGINAL import kotlin.native.ObjCName @ObjCName(name = "MonsterDetailOptionState", exact = true) data class MonsterDetailOptionState( - val id: String = "", + val id: MonsterDetailOptionStateId = ADD_TO_FOLDER, val name: String = "", ) { companion object { - internal const val ADD_TO_FOLDER = "add_to_folder" - internal const val CLONE = "clone" - internal const val EDIT = "edit" - 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 Export(strings: MonsterDetailStrings): MonsterDetailOptionState { + return MonsterDetailOptionState( + id = EXPORT, + name = strings.export + ) + } @Suppress("FunctionName") internal fun ResetToOriginal(strings: MonsterDetailStrings): MonsterDetailOptionState { @@ -90,3 +98,14 @@ data class MonsterDetailOptionState( } } } + +enum class MonsterDetailOptionStateId { + ADD_TO_FOLDER, + CLONE, + EDIT, + DELETE, + CHANGE_TO_FEET, + CHANGE_TO_METERS, + RESET_TO_ORIGINAL, + EXPORT, +} 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 3a7249174..e29f35cfd 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 @@ -16,6 +16,8 @@ package br.alexandregpereira.hunter.monster.detail +import br.alexadregpereira.hunter.shareContent.event.ShareContentEvent +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher import br.alexandregpereira.hunter.domain.model.MeasurementUnit import br.alexandregpereira.hunter.domain.model.Monster import br.alexandregpereira.hunter.domain.model.MonsterStatus @@ -27,18 +29,20 @@ import br.alexandregpereira.hunter.event.folder.insert.FolderInsertEventDispatch import br.alexandregpereira.hunter.event.monster.lore.detail.MonsterLoreDetailEvent import br.alexandregpereira.hunter.event.monster.lore.detail.MonsterLoreDetailEventDispatcher import br.alexandregpereira.hunter.localization.AppLocalization -import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.ADD_TO_FOLDER import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.AddToFolder -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.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.Export import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionState.Companion.ResetToOriginal +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.ADD_TO_FOLDER +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.CHANGE_TO_FEET +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.CHANGE_TO_METERS +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.CLONE +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.DELETE +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.EDIT +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.EXPORT +import br.alexandregpereira.hunter.monster.detail.MonsterDetailOptionStateId.RESET_TO_ORIGINAL import br.alexandregpereira.hunter.monster.detail.domain.CloneMonsterUseCase import br.alexandregpereira.hunter.monster.detail.domain.DeleteMonsterUseCase import br.alexandregpereira.hunter.monster.detail.domain.GetMonsterDetailUseCase @@ -82,6 +86,7 @@ class MonsterDetailStateHolder internal constructor( private val resetMonsterToOriginal: ResetMonsterToOriginal, private val spellDetailEventDispatcher: SpellDetailEventDispatcher, private val monsterEventDispatcher: MonsterEventDispatcher, + private val shareContentEventDispatcher: ShareContentEventDispatcher, private val monsterLoreDetailEventDispatcher: MonsterLoreDetailEventDispatcher, private val folderInsertEventDispatcher: FolderInsertEventDispatcher, private val monsterRegistrationEventDispatcher: EventDispatcher, @@ -209,6 +214,13 @@ class MonsterDetailStateHolder internal constructor( CHANGE_TO_METERS -> { changeMeasurementUnit(MeasurementUnit.METER) } + + EXPORT -> { + analytics.trackMonsterDetailExportClicked(monsterIndex) + shareContentEventDispatcher.dispatchEvent( + ShareContentEvent.Export.OnStart(monsterIndex) + ) + } } } @@ -329,12 +341,13 @@ class MonsterDetailStateHolder internal constructor( val monster = metadata.find { monster -> monster.index == monsterIndex } ?: return this val editOption = when (monster.status) { MonsterStatus.Original -> listOf(Edit(strings)) + MonsterStatus.Imported, MonsterStatus.Clone -> listOf(Edit(strings), Delete(strings)) MonsterStatus.Edited -> listOf(Edit(strings), ResetToOriginal(strings)) } return copy( - options = listOf(AddToFolder(strings), Clone(strings)) + editOption + options = listOf(AddToFolder(strings), Export(strings), Clone(strings)) + editOption ) } 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 6c133ae08..471e87477 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 @@ -47,6 +47,7 @@ interface MonsterDetailStrings { val resetToOriginal: String val resetQuestion: String val resetConfirmation: String + val export: String } internal data class MonsterDetailEnStrings( @@ -91,6 +92,7 @@ internal data class MonsterDetailEnStrings( 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", + override val export: String = "Share", ) : MonsterDetailStrings internal data class MonsterDetailPtStrings( @@ -135,6 +137,7 @@ internal data class MonsterDetailPtStrings( 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", + override val export: String = "Compartilhar", ) : 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 e216ca04d..b000d6c69 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 @@ -53,6 +53,7 @@ val monsterDetailModule = module { stateRecovery = get(named(MonsterDetailStateRecoveryQualifier)), resetMonsterToOriginal = get(), syncEventDispatcher = get(), + shareContentEventDispatcher = get(), ) } factory { CloneMonsterUseCase(get(), get(), get(), 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 index 4a50b5d4a..9dcf7821a 100644 --- 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 @@ -15,6 +15,7 @@ internal fun SaveMonsterUseCase( val newMonster = when (monster.status) { MonsterStatus.Original -> monster.copy(status = MonsterStatus.Edited) MonsterStatus.Edited, + MonsterStatus.Imported, MonsterStatus.Clone -> monster } saveMonsters(listOf(newMonster)) diff --git a/feature/settings/compose/build.gradle.kts b/feature/settings/compose/build.gradle.kts index d5ae3a166..9857bd0cd 100644 --- a/feature/settings/compose/build.gradle.kts +++ b/feature/settings/compose/build.gradle.kts @@ -14,6 +14,7 @@ multiplatform { implementation(project(":domain:monster:core")) implementation(project(":domain:monster:event")) implementation(project(":domain:settings:core")) + implementation(project(":feature:share-content:event")) implementation(project(":feature:monster-content-manager:event")) implementation(project(":feature:sync:event")) implementation(project(":ui:core")) diff --git a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStateHolder.kt b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStateHolder.kt index 11b7d2202..819be4ca9 100644 --- a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStateHolder.kt +++ b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStateHolder.kt @@ -16,6 +16,8 @@ package br.alexandregpereira.hunter.settings +import br.alexadregpereira.hunter.shareContent.event.ShareContentEvent +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher import br.alexandregpereira.hunter.domain.settings.AppearanceSettings import br.alexandregpereira.hunter.domain.settings.GetAlternativeSourceJsonUrlUseCase import br.alexandregpereira.hunter.domain.settings.GetMonsterImageJsonUrlUseCase @@ -54,6 +56,7 @@ internal class SettingsStateHolder( private val dispatcher: CoroutineDispatcher, private val syncEventDispatcher: SyncEventDispatcher, private val monsterEventDispatcher: MonsterEventDispatcher, + private val shareContentEventDispatcher: ShareContentEventDispatcher, private val analytics: SettingsAnalytics, private val bottomBarEventDispatcher: EventDispatcher, private val appLocalization: AppLocalization, @@ -175,6 +178,10 @@ internal class SettingsStateHolder( setState { copy(appearanceState = appearance) } } + override fun onImport() { + shareContentEventDispatcher.dispatchEvent(ShareContentEvent.Import.OnStart) + } + private fun load() { getMonsterImageJsonUrl() .zip(getAlternativeSourceJsonUrl()) { imageBaseUrl, alternativeSourceBaseUrl -> diff --git a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStrings.kt b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStrings.kt index a5d81ba7d..b93f9ba3e 100644 --- a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStrings.kt +++ b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsStrings.kt @@ -16,6 +16,7 @@ interface SettingsStrings { val forceLightImageBackground: String val defaultLightBackground: String val defaultDarkBackground: String + val importContent: String } @@ -33,6 +34,7 @@ internal data class SettingsEnStrings( override val forceLightImageBackground: String = "Use Light Background Color in Images", override val defaultLightBackground: String = "Default Light Background Color", override val defaultDarkBackground: String = "Default Dark Background Color", + override val importContent: String = "Import Shared Content", ) : SettingsStrings internal data class SettingsPtStrings( @@ -49,6 +51,7 @@ internal data class SettingsPtStrings( override val forceLightImageBackground: String = "Usar Cor de Fundo Claro nas Imagens", override val defaultLightBackground: String = "Cor Padrão de Fundo das Imagens Light", override val defaultDarkBackground: String = "Cor Padrão de Fundo das Imagens Dark", + override val importContent: String = "Importar Conteúdo Compartilhado", ) : SettingsStrings internal data class SettingsEmptyStrings( @@ -65,6 +68,7 @@ internal data class SettingsEmptyStrings( override val forceLightImageBackground: String = "", override val defaultLightBackground: String = "", override val defaultDarkBackground: String = "", + override val importContent: String = "", ) : SettingsStrings internal fun getSettingsStrings(lang: Language): SettingsStrings { diff --git a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsViewIntent.kt b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsViewIntent.kt index 143f11ab5..2b7db132f 100644 --- a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsViewIntent.kt +++ b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/SettingsViewIntent.kt @@ -28,4 +28,6 @@ internal interface SettingsViewIntent { fun onAppearanceSettingsSaveClick() fun onAppearanceChange(appearance: AppearanceSettingsState) + + fun onImport() } diff --git a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/di/Module.kt b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/di/Module.kt index 0449fe332..df0800434 100644 --- a/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/di/Module.kt +++ b/feature/settings/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/settings/di/Module.kt @@ -40,6 +40,7 @@ val featureSettingsModule = module { applyAppearanceSettings = get(), getAppearanceSettings = get(), monsterEventDispatcher = get(), + shareContentEventDispatcher = get(), ) } } 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 3b00b0621..d6ced6010 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 @@ -61,6 +61,13 @@ internal fun MenuScreen( Divider() + MenuItem( + text = state.strings.importContent, + onClick = viewIntent::onImport + ) + + Divider() + MenuItem( text = state.strings.settingsTitle, onClick = viewIntent::onSettingsClick diff --git a/feature/share-content/compose/.gitignore b/feature/share-content/compose/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/feature/share-content/compose/.gitignore @@ -0,0 +1 @@ +/build diff --git a/feature/share-content/compose/build.gradle.kts b/feature/share-content/compose/build.gradle.kts new file mode 100644 index 000000000..d7f104885 --- /dev/null +++ b/feature/share-content/compose/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.library") + kotlin("multiplatform") + kotlin("plugin.serialization") + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) +} + +multiplatform { + androidMain() + commonMain { + implementation(project(":domain:monster:core")) + implementation(project(":domain:monster-lore:core")) + implementation(project(":domain:spell:core")) + implementation(project(":core:localization")) + implementation(project(":core:analytics")) + implementation(project(":core:event")) + implementation(project(":core:state-holder")) + implementation(project(":feature:share-content:event")) + implementation(project(":ui:core")) + + implementation(libs.kotlin.coroutines.core) + implementation(libs.koin.compose) + implementation(libs.kotlin.serialization) + } + jvmMain() + jvmTest { + implementation(kotlin("test")) + implementation(libs.kotlin.coroutines.test) + } + iosMain() +} + +androidLibrary { + namespace = "br.alexandregpereira.hunter.shareContent" +} + +composeCompiler { + enableStrongSkippingMode = true +} + +compose.resources { + publicResClass = false + packageOfResClass = "br.alexandregpereira.hunter.shareContent.ui.resources" + generateResClass = always +} diff --git a/feature/share-content/compose/src/commonMain/composeResources/drawable/IconContentPaste.xml b/feature/share-content/compose/src/commonMain/composeResources/drawable/IconContentPaste.xml new file mode 100644 index 000000000..b2e608b66 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/composeResources/drawable/IconContentPaste.xml @@ -0,0 +1,11 @@ + + + + diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentExportMonsterFeature.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentExportMonsterFeature.kt new file mode 100644 index 000000000..b704f0c9d --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentExportMonsterFeature.kt @@ -0,0 +1,79 @@ +package br.alexandregpereira.hunter.shareContent + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import br.alexadregpereira.hunter.shareContent.event.ShareContentEvent.Export +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher +import br.alexadregpereira.hunter.shareContent.event.exportEvents +import br.alexandregpereira.hunter.shareContent.state.ShareContentStateHolder +import br.alexandregpereira.hunter.shareContent.state.ShareContentUiEvent +import br.alexandregpereira.hunter.shareContent.ui.ShareContentExportScreen +import br.alexandregpereira.hunter.ui.compose.BottomSheet +import org.koin.compose.koinInject + +@Composable +fun ShareContentExportMonsterFeature( + contentPadding: PaddingValues, +) { + val eventDispatcher = koinInject() + var isOpen by rememberSaveable { + mutableStateOf(false) + } + var monsterIndex by rememberSaveable { + mutableStateOf("") + } + LaunchedEffect(eventDispatcher.events) { + eventDispatcher.exportEvents().collect { event -> + isOpen = when (event) { + is Export.OnStart -> { + monsterIndex = event.monsterIndex + true + } + is Export.OnFinish -> false + } + } + } + BottomSheet( + contentPadding = PaddingValues( + end = 16.dp, + start = 16.dp, + bottom = 16.dp + contentPadding.calculateBottomPadding(), + ), + opened = isOpen, + onClose = { isOpen = false }, + ) { + val stateHolder = koinInject() + val clipboardManager = LocalClipboardManager.current + LaunchedEffect(monsterIndex) { + stateHolder.fetchMonsterContentToExport( + monsterIndex = monsterIndex, + actualClipboardContent = clipboardManager.getText()?.text + ) + } + + val state by stateHolder.state.collectAsState() + ShareContentExportScreen( + state = state, + onCopy = stateHolder::onCopyContentToExport, + ) + + LaunchedEffect(stateHolder.action) { + stateHolder.action.collect { action -> + when (action) { + is ShareContentUiEvent.CopyContentUiToExport -> { + clipboardManager.setText(AnnotatedString(action.content)) + } + } + } + } + } +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentImportFeature.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentImportFeature.kt new file mode 100644 index 000000000..92109046a --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ShareContentImportFeature.kt @@ -0,0 +1,55 @@ +package br.alexandregpereira.hunter.shareContent + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import br.alexadregpereira.hunter.shareContent.event.ShareContentEvent.Import +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher +import br.alexadregpereira.hunter.shareContent.event.importEvents +import br.alexandregpereira.hunter.shareContent.state.ShareContentStateHolder +import br.alexandregpereira.hunter.shareContent.ui.ShareContentImportScreen +import br.alexandregpereira.hunter.ui.compose.BottomSheet +import org.koin.compose.koinInject + +@Composable +fun ShareContentImportFeature( + contentPadding: PaddingValues, +) { + val eventDispatcher = koinInject() + var isOpen by rememberSaveable { + mutableStateOf(false) + } + LaunchedEffect(eventDispatcher.events) { + eventDispatcher.importEvents().collect { event -> + isOpen = when (event) { + is Import.OnStart -> true + is Import.OnFinish -> false + } + } + } + BottomSheet( + contentPadding = PaddingValues( + end = 16.dp, + start = 16.dp, + bottom = 16.dp + contentPadding.calculateBottomPadding(), + ), + opened = isOpen, + onClose = { isOpen = false }, + modifier = Modifier.animateContentSize() + ) { + val stateHolder = koinInject() + ShareContentImportScreen( + state = stateHolder.state.collectAsState().value, + onImport = stateHolder::onImport, + onPaste = stateHolder::onPasteImportContent, + ) + } +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/di.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/di.kt new file mode 100644 index 000000000..46ddfb162 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/di.kt @@ -0,0 +1,14 @@ +package br.alexandregpereira.hunter.shareContent + +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher +import br.alexandregpereira.hunter.shareContent.domain.GetMonsterContentToExport +import br.alexandregpereira.hunter.shareContent.domain.ImportContent +import br.alexandregpereira.hunter.shareContent.state.ShareContentStateHolder +import org.koin.dsl.module + +val featureShareContentModule = module { + single { ShareContentEventDispatcher() } + factory { ImportContent(get(), get(), get()) } + factory { GetMonsterContentToExport(get(), get(), get()) } + single { ShareContentStateHolder(get(), get(), get(), get(), get()) } +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/GetMonsterContentToExportUseCase.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/GetMonsterContentToExportUseCase.kt new file mode 100644 index 000000000..483d8e10f --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/GetMonsterContentToExportUseCase.kt @@ -0,0 +1,43 @@ +package br.alexandregpereira.hunter.shareContent.domain + +import br.alexandregpereira.hunter.domain.model.Monster +import br.alexandregpereira.hunter.domain.monster.lore.GetMonsterLoreUseCase +import br.alexandregpereira.hunter.domain.spell.GetSpellsByIdsUseCase +import br.alexandregpereira.hunter.domain.usecase.GetMonsterUseCase +import br.alexandregpereira.hunter.shareContent.domain.mapper.toShareMonster +import br.alexandregpereira.hunter.shareContent.domain.mapper.toShareMonsterLore +import br.alexandregpereira.hunter.shareContent.domain.mapper.toShareSpell +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.serialization.encodeToString + +internal fun interface GetMonsterContentToExport { + + operator fun invoke(monsterIndex: String): Flow +} + +internal fun GetMonsterContentToExport( + getMonster: GetMonsterUseCase, + getMonsterLore: GetMonsterLoreUseCase, + getSpellsByIds: GetSpellsByIdsUseCase +): GetMonsterContentToExport = GetMonsterContentToExport { monsterIndex -> + getMonster(monsterIndex).map { monster -> + val monsterLore = getMonsterLore(monsterIndex).singleOrNull() + val spells = getSpellsByIds(monster.getSpellIndexes()).single().takeIf { it.isNotEmpty() } + + val shareContent = ShareContent( + monsters = listOf(monster.toShareMonster()), + monstersLore = listOfNotNull(monsterLore?.toShareMonsterLore()), + spells = spells?.map { it.toShareSpell() }, + ) + json.encodeToString(shareContent) + } +} + +private fun Monster.getSpellIndexes(): List { + return spellcastings.asSequence().map { + it.usages + }.flatten().map { it.spells }.flatten().map { it.index }.toList() +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/ImportContentUseCase.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/ImportContentUseCase.kt new file mode 100644 index 000000000..92847abde --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/ImportContentUseCase.kt @@ -0,0 +1,78 @@ +package br.alexandregpereira.hunter.shareContent.domain + +import br.alexandregpereira.hunter.domain.monster.lore.SaveMonstersLoreUseCase +import br.alexandregpereira.hunter.domain.spell.SaveSpells +import br.alexandregpereira.hunter.domain.usecase.SaveMonstersUseCase +import br.alexandregpereira.hunter.shareContent.domain.ShareContent.Companion.CURRENT_VERSION +import br.alexandregpereira.hunter.shareContent.domain.mapper.toMonster +import br.alexandregpereira.hunter.shareContent.domain.mapper.toMonsterLore +import br.alexandregpereira.hunter.shareContent.domain.mapper.toSpell +import br.alexandregpereira.hunter.shareContent.domain.model.ShareMonster +import br.alexandregpereira.hunter.shareContent.domain.model.ShareMonsterLore +import br.alexandregpereira.hunter.shareContent.domain.model.ShareSpell +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.single +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +internal val json = Json { + ignoreUnknownKeys = true + explicitNulls = false +} + +internal fun interface ImportContent { + operator fun invoke(contentJson: String): Flow +} + +internal fun ImportContent( + saveMonsters: SaveMonstersUseCase, + saveSpells: SaveSpells, + saveMonstersLore: SaveMonstersLoreUseCase, +): ImportContent = ImportContent { contentJson -> + flow { + val content = runCatching { json.decodeFromString(contentJson) } + .getOrElse { cause -> + when (cause) { + is SerializationException -> throw ImportContentException.InvalidContent( + content = contentJson, + cause = cause, + ) + else -> throw cause + } + } + content.monsters?.let { monsters -> + saveMonsters(monsters.map { it.toMonster() }).single() + } + content.monstersLore?.let { monstersLore -> + saveMonstersLore(monstersLore.map { it.toMonsterLore() }, isSync = false).single() + } + content.spells?.let { spells -> + saveSpells(spells.map { it.toSpell() }).single() + } + emit(Unit) + } +} + +@Serializable +internal data class ShareContent( + val monsters: List? = null, + val monstersLore: List? = null, + val spells: List? = null, +) { + val version: Int = CURRENT_VERSION + + companion object { + const val CURRENT_VERSION = 1 + } +} + +internal sealed class ImportContentException(message: String) : RuntimeException(message) { + class InvalidContent(content: String, cause: Throwable) : ImportContentException( + message = "SerializationException. " + + "cause = ${cause.message}" + + "current content version = $CURRENT_VERSION, " + + "content imported version= ${content.replace("\n", "")}" + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterLoreMapper.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterLoreMapper.kt new file mode 100644 index 000000000..106486c71 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterLoreMapper.kt @@ -0,0 +1,17 @@ +package br.alexandregpereira.hunter.shareContent.domain.mapper + +import br.alexandregpereira.hunter.domain.monster.lore.model.MonsterLore +import br.alexandregpereira.hunter.shareContent.domain.model.ShareMonsterLore +import br.alexandregpereira.hunter.shareContent.domain.model.ShareMonsterLoreEntry + +internal fun MonsterLore.toShareMonsterLore(): ShareMonsterLore { + return ShareMonsterLore( + index = index, + entries = entries.map { + ShareMonsterLoreEntry( + title = it.title, + description = it.description, + ) + } + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterMapper.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterMapper.kt new file mode 100644 index 000000000..77e7030cb --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareMonsterMapper.kt @@ -0,0 +1,126 @@ +package br.alexandregpereira.hunter.shareContent.domain.mapper + +import br.alexandregpereira.hunter.domain.model.AbilityDescription +import br.alexandregpereira.hunter.domain.model.AbilityScore +import br.alexandregpereira.hunter.domain.model.Action +import br.alexandregpereira.hunter.domain.model.Damage +import br.alexandregpereira.hunter.domain.model.DamageDice +import br.alexandregpereira.hunter.domain.model.Monster +import br.alexandregpereira.hunter.domain.model.SavingThrow +import br.alexandregpereira.hunter.domain.model.Skill +import br.alexandregpereira.hunter.domain.model.SpeedValue +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 br.alexandregpereira.hunter.shareContent.domain.model.* + +internal fun Monster.toShareMonster(): ShareMonster { + return ShareMonster( + index = index, + name = name, + type = type.name, + challengeRating = challengeRating, + imageUrl = imageData.url, + imageBackgroundColorLight = imageData.backgroundColor.light, + imageBackgroundColorDark = imageData.backgroundColor.dark, + isHorizontalImage = imageData.isHorizontal, + subtype = subtype, + group = group, + subtitle = subtitle, + size = size, + alignment = alignment, + armorClass = stats.armorClass, + hitPoints = stats.hitPoints, + hitDice = stats.hitDice, + senses = senses, + languages = languages, + sourceName = sourceName, + hover = speed.hover, + speedValues = speed.values.map { it.toShareSpeedValue() }, + abilityScores = abilityScores.map { it.toShareAbilityScore() }, + savingThrows = savingThrows.map { it.toShareSavingThrow() }, + skills = skills.map { it.toShareSkill() }, + damageVulnerabilities = damageVulnerabilities.map { it.toShareType() }, + damageResistances = damageResistances.map { it.toShareType() }, + damageImmunities = damageImmunities.map { it.toShareType() }, + conditionImmunities = conditionImmunities.map { + ShareType( + index = it.index, + type = it.type.name, + name = it.name + ) + }, + specialAbilities = specialAbilities.map { it.toShareType() }, + actions = actions.map { it.toShareAction() }, + legendaryActions = legendaryActions.map { it.toShareAction() }, + reactions = reactions.map { it.toShareType() }, + spellcaster = spellcastings.find { it.type == SpellcastingType.SPELLCASTER } + ?.toShareSpellcasting(), + innateSpellcaster = spellcastings.find { it.type == SpellcastingType.INNATE } + ?.toShareSpellcasting(), + lore = lore, + ) +} + +private fun SpeedValue.toShareSpeedValue(): ShareSpeedValue = ShareSpeedValue( + type = type.name, + valueFormatted = valueFormatted, + index = index +) + +private fun AbilityScore.toShareAbilityScore(): ShareAbilityScore = ShareAbilityScore( + type = type.name, + value = value, + modifier = modifier +) + +private fun SavingThrow.toShareSavingThrow(): ShareSavingThrow = ShareSavingThrow( + index = index, + modifier = modifier, + type = type.name +) + +private fun Skill.toShareSkill(): ShareSavingThrow = ShareSavingThrow( + index = index, + modifier = modifier, + type = name, +) + +private fun Damage.toShareType(): ShareType = ShareType( + index = index, + type = type.name, + name = name +) + +private fun AbilityDescription.toShareType(): ShareType = ShareType( + index = index, + type = description, + name = name +) + +private fun Action.toShareAction(): ShareAction = ShareAction( + id = id, + damageDices = damageDices.map { it.toShareDamageDice() }, + attackBonus = attackBonus, + abilityDescription = abilityDescription.toShareType() +) + +private fun DamageDice.toShareDamageDice(): ShareDamageDice = ShareDamageDice( + dice = dice, + damage = damage.toShareType(), +) + +private fun Spellcasting.toShareSpellcasting(): ShareSpellcasting = ShareSpellcasting( + description = description, + usages = usages.map { it.toShareSpellUsage() }, +) + +private fun SpellUsage.toShareSpellUsage(): ShareSpellUsage = ShareSpellUsage( + group = group, + spells = spells.map { + ShareMonsterSpell( + index = it.index, + name = it.name, + ) + }, +) diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareSpellMapper.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareSpellMapper.kt new file mode 100644 index 000000000..6934a37aa --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/DomainToShareSpellMapper.kt @@ -0,0 +1,23 @@ +package br.alexandregpereira.hunter.shareContent.domain.mapper + +import br.alexandregpereira.hunter.domain.spell.model.Spell +import br.alexandregpereira.hunter.shareContent.domain.model.ShareSpell + +internal fun Spell.toShareSpell(): ShareSpell { + return ShareSpell( + index = index, + name = name, + level = level, + castingTime = castingTime, + components = components, + duration = duration, + range = range, + ritual = ritual, + concentration = concentration, + savingThrowType = savingThrowType?.name, + damageType = damageType, + school = school.name, + description = description, + higherLevel = higherLevel, + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterLoreToDomainMapper.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterLoreToDomainMapper.kt new file mode 100644 index 000000000..0f24fed8e --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterLoreToDomainMapper.kt @@ -0,0 +1,18 @@ +package br.alexandregpereira.hunter.shareContent.domain.mapper + +import br.alexandregpereira.hunter.domain.monster.lore.model.MonsterLore +import br.alexandregpereira.hunter.domain.monster.lore.model.MonsterLoreEntry +import br.alexandregpereira.hunter.shareContent.domain.model.ShareMonsterLore + +internal fun ShareMonsterLore.toMonsterLore(): MonsterLore { + return MonsterLore( + index = index, + name = "", + entries = entries.map { + MonsterLoreEntry( + title = it.title, + description = it.description, + ) + } + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterToDomainMapper.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterToDomainMapper.kt new file mode 100644 index 000000000..dad816a4a --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareMonsterToDomainMapper.kt @@ -0,0 +1,162 @@ +package br.alexandregpereira.hunter.shareContent.domain.mapper + +import br.alexandregpereira.hunter.domain.model.AbilityDescription +import br.alexandregpereira.hunter.domain.model.AbilityScore +import br.alexandregpereira.hunter.domain.model.AbilityScoreType +import br.alexandregpereira.hunter.domain.model.Action +import br.alexandregpereira.hunter.domain.model.Color +import br.alexandregpereira.hunter.domain.model.Condition +import br.alexandregpereira.hunter.domain.model.ConditionType +import br.alexandregpereira.hunter.domain.model.Damage +import br.alexandregpereira.hunter.domain.model.DamageDice +import br.alexandregpereira.hunter.domain.model.DamageType +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.SavingThrow +import br.alexandregpereira.hunter.domain.model.Skill +import br.alexandregpereira.hunter.domain.model.Speed +import br.alexandregpereira.hunter.domain.model.SpeedType +import br.alexandregpereira.hunter.domain.model.SpeedValue +import br.alexandregpereira.hunter.domain.model.Stats +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 br.alexandregpereira.hunter.shareContent.domain.model.ShareAction +import br.alexandregpereira.hunter.shareContent.domain.model.ShareMonster +import br.alexandregpereira.hunter.shareContent.domain.model.ShareSpellcasting +import br.alexandregpereira.hunter.shareContent.domain.model.ShareType + +internal fun ShareMonster.toMonster(): Monster { + return Monster( + index = index, + name = name, + type = MonsterType.valueOf(type), + challengeRating = challengeRating, + imageData = MonsterImageData( + url = imageUrl, + backgroundColor = Color( + light = imageBackgroundColorLight, + dark = imageBackgroundColorDark + ), + isHorizontal = isHorizontalImage + ), + subtype = subtype, + group = group, + subtitle = subtitle, + size = size, + alignment = alignment, + stats = Stats( + armorClass = armorClass, + hitPoints = hitPoints, + hitDice = hitDice + ), + senses = senses, + languages = languages, + sourceName = sourceName, + speed = Speed( + hover = hover, + values = speedValues.map { + SpeedValue( + type = SpeedType.valueOf(it.type), + valueFormatted = it.valueFormatted, + index = it.index + ) + } + ), + abilityScores = abilityScores.map { + AbilityScore( + type = AbilityScoreType.valueOf(it.type), + value = it.value, + modifier = it.modifier + ) + }, + savingThrows = savingThrows.map { + SavingThrow( + index = it.index, + modifier = it.modifier, + type = AbilityScoreType.valueOf(it.type) + ) + }, + skills = skills.map { + Skill( + index = it.index, + modifier = it.modifier, + name = it.type + ) + }, + damageVulnerabilities = damageVulnerabilities.map { it.toDamage() }, + damageResistances = damageResistances.map { it.toDamage() }, + damageImmunities = damageImmunities.map { it.toDamage() }, + conditionImmunities = conditionImmunities.map { + Condition( + index = it.index, + type = ConditionType.valueOf(it.type), + name = it.name, + ) + }, + specialAbilities = specialAbilities.map { it.toAbilityDescription() }, + actions = actions.map { it.toAction() }, + legendaryActions = legendaryActions.map { it.toAction() }, + reactions = reactions.map { it.toAbilityDescription() }, + spellcastings = listOfNotNull( + spellcaster?.toSpellcastings(SpellcastingType.SPELLCASTER), + innateSpellcaster?.toSpellcastings(SpellcastingType.INNATE), + ), + lore = lore, + status = MonsterStatus.Imported, + ) +} + +private fun ShareType.toDamage(): Damage { + return Damage( + index = index, + type = DamageType.valueOf(type), + name = name, + ) +} + +private fun ShareType.toAbilityDescription(): AbilityDescription { + return AbilityDescription( + index = index, + description = type, + name = name, + ) +} + +private fun ShareAction.toAction(): Action { + return Action( + id = id, + damageDices = damageDices.map { + DamageDice( + dice = it.dice, + damage = it.damage.toDamage(), + ) + }, + attackBonus = attackBonus, + abilityDescription = abilityDescription.toAbilityDescription(), + ) +} + +private fun ShareSpellcasting.toSpellcastings(type: SpellcastingType): Spellcasting { + return Spellcasting( + description = description, + type = type, + usages = usages.map { + SpellUsage( + group = it.group, + spells = it.spells.map { spell -> + SpellPreview( + index = spell.index, + name = spell.name, + level = 0, + school = SchoolOfMagic.ABJURATION, + ) + }, + ) + } + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareSpellToDomainMapper.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareSpellToDomainMapper.kt new file mode 100644 index 000000000..d9cf6b59d --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/mapper/ShareSpellToDomainMapper.kt @@ -0,0 +1,27 @@ +package br.alexandregpereira.hunter.shareContent.domain.mapper + +import br.alexandregpereira.hunter.domain.spell.model.SavingThrowType +import br.alexandregpereira.hunter.domain.spell.model.SchoolOfMagic +import br.alexandregpereira.hunter.domain.spell.model.Spell +import br.alexandregpereira.hunter.domain.spell.model.SpellStatus +import br.alexandregpereira.hunter.shareContent.domain.model.ShareSpell + +internal fun ShareSpell.toSpell(): Spell { + return Spell( + index = index, + name = name, + level = level, + castingTime = castingTime, + components = components, + duration = duration, + range = range, + ritual = ritual, + concentration = concentration, + savingThrowType = savingThrowType?.let { SavingThrowType.valueOf(it) }, + damageType = damageType, + school = SchoolOfMagic.valueOf(school), + description = description, + higherLevel = higherLevel, + status = SpellStatus.Imported, + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonster.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonster.kt new file mode 100644 index 000000000..1e472a91c --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonster.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2024 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.shareContent.domain.model + +import kotlinx.serialization.Serializable + +@Serializable +internal data class ShareMonster( + val index: String, + val name: String = "", + val type: String = "", + val challengeRating: Float = 0f, + val imageUrl: String = "", + val imageBackgroundColorLight: String = "", + val imageBackgroundColorDark: String = "", + val isHorizontalImage: Boolean = false, + val subtype: String? = null, + val group: String? = null, + val subtitle: String = "", + val size: String = "", + val alignment: String = "", + val armorClass: Int = 0, + val hitPoints: Int = 0, + val hitDice: String = "", + val senses: List = emptyList(), + val languages: String = "", + val sourceName: String = "", + val hover: Boolean = false, + val speedValues: List, + val abilityScores: List = emptyList(), + val savingThrows: List = emptyList(), + val skills: List = emptyList(), + val damageVulnerabilities: List = emptyList(), + val damageResistances: List = emptyList(), + val damageImmunities: List = emptyList(), + val conditionImmunities: List = emptyList(), + val specialAbilities: List = emptyList(), + val actions: List = emptyList(), + val legendaryActions: List = emptyList(), + val reactions: List = emptyList(), + val spellcaster: ShareSpellcasting? = null, + val innateSpellcaster: ShareSpellcasting? = null, + val lore: String? = null, +) + +@Serializable +internal data class ShareSpeedValue( + val type: String = "", + val valueFormatted: String, + val index: String = "", +) + +@Serializable +internal data class ShareAbilityScore( + val type: String, + val value: Int, + val modifier: Int +) + +@Serializable +internal data class ShareSavingThrow( + val index: String, + val modifier: Int, + val type: String +) + +@Serializable +internal data class ShareType( + val index: String, + val type: String, + val name: String +) + +@Serializable +internal data class ShareAction( + val id: String, + val damageDices: List, + val attackBonus: Int? = null, + val abilityDescription: ShareType, +) + +@Serializable +internal data class ShareDamageDice( + val dice: String, + val damage: ShareType, +) + +@Serializable +internal data class ShareSpellcasting( + val description: String, + val usages: List, +) + +@Serializable +internal data class ShareSpellUsage( + val group: String, + val spells: List, +) + +@Serializable +internal data class ShareMonsterSpell( + val index: String, + val name: String, +) diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonsterLore.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonsterLore.kt new file mode 100644 index 000000000..e15e2f752 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareMonsterLore.kt @@ -0,0 +1,15 @@ +package br.alexandregpereira.hunter.shareContent.domain.model + +import kotlinx.serialization.Serializable + +@Serializable +internal data class ShareMonsterLore( + val index: String, + val entries: List, +) + +@Serializable +internal data class ShareMonsterLoreEntry( + val title: String? = null, + val description: String, +) diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareSpell.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareSpell.kt new file mode 100644 index 000000000..7485a9053 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/domain/model/ShareSpell.kt @@ -0,0 +1,21 @@ +package br.alexandregpereira.hunter.shareContent.domain.model + +import kotlinx.serialization.Serializable + +@Serializable +internal data class ShareSpell( + val index: String, + val name: String, + val level: Int, + val castingTime: String, + val components: String, + val duration: String, + val range: String, + val ritual: Boolean, + val concentration: Boolean, + val savingThrowType: String?, + val damageType: String?, + val school: String, + val description: String, + val higherLevel: String?, +) diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentState.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentState.kt new file mode 100644 index 000000000..cafe79cb6 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentState.kt @@ -0,0 +1,32 @@ +package br.alexandregpereira.hunter.shareContent.state + +internal data class ShareContentState( + val contentToImport: String = "", + val contentToExport: String = "", + val exportCopyButtonEnabled: Boolean = false, + val importError: ShareContentImportError? = null, + val strings: ShareContentStrings = ShareContentStrings(), +) { + val exportCopyButtonText: String = strings.copyButton.takeIf { exportCopyButtonEnabled } + ?: strings.copiedButton + + val importErrorMessage: String = importError?.let { + when (it) { + ShareContentImportError.InvalidContent -> strings.importInvalidContentErrorMessage + } + } ?: "" + + val contentToExportShort: String = contentToExport.take(1000) + .takeIf { it.isNotBlank() }?.let { "$it..." }.orEmpty() + + val contentToImportShort: String = contentToImport.take(1000) + .takeIf { it.isNotBlank() } + ?.replace("\t", "") + ?.replace("\n", "") + ?.let { "$it..." } + .orEmpty() +} + +internal enum class ShareContentImportError { + InvalidContent +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStateHolder.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStateHolder.kt new file mode 100644 index 000000000..0d48fdc14 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStateHolder.kt @@ -0,0 +1,80 @@ +package br.alexandregpereira.hunter.shareContent.state + +import br.alexadregpereira.hunter.shareContent.event.ShareContentEvent.* +import br.alexadregpereira.hunter.shareContent.event.ShareContentEventDispatcher +import br.alexandregpereira.hunter.localization.AppReactiveLocalization +import br.alexandregpereira.hunter.shareContent.domain.GetMonsterContentToExport +import br.alexandregpereira.hunter.shareContent.domain.ImportContent +import br.alexandregpereira.hunter.shareContent.domain.ImportContentException +import br.alexandregpereira.hunter.shareContent.state.ShareContentImportError.* +import br.alexandregpereira.hunter.state.MutableActionHandler +import br.alexandregpereira.hunter.state.UiModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class ShareContentStateHolder( + private val dispatcher: CoroutineDispatcher, + appLocalization: AppReactiveLocalization, + private val eventDispatcher: ShareContentEventDispatcher, + private val importContent: ImportContent, + private val getMonsterContentToExport: GetMonsterContentToExport, +) : UiModel( + ShareContentState(strings = appLocalization.getLanguage().getStrings()) +), MutableActionHandler by MutableActionHandler() { + + init { + appLocalization.languageFlow + .onEach { setState { copy(strings = it.getStrings()) } } + .launchIn(scope) + } + + fun onImport() { + if (state.value.contentToImport.isBlank()) { + return + } + importContent(state.value.contentToImport) + .flowOn(dispatcher) + .onEach { + eventDispatcher.dispatchEvent(Import.OnFinish) + delay(1000) + setState { copy(contentToImport = "", importError = null) } + } + .catch { cause -> + if (cause is ImportContentException) { + cause.printStackTrace() + val importError = when (cause) { + is ImportContentException.InvalidContent -> InvalidContent + } + setState { copy(importError = importError) } + } else throw cause + } + .launchIn(scope) + } + + fun onPasteImportContent(content: String) = setState { + copy(contentToImport = content.trim(), importError = null) + } + + fun fetchMonsterContentToExport(monsterIndex: String, actualClipboardContent: String?) { + getMonsterContentToExport(monsterIndex) + .flowOn(dispatcher) + .onEach { contentToExport -> + setState { + copy( + contentToExport = contentToExport, + exportCopyButtonEnabled = contentToExport != actualClipboardContent, + ) + } + } + .launchIn(scope) + } + + fun onCopyContentToExport() { + setState { copy(exportCopyButtonEnabled = false) } + sendAction(ShareContentUiEvent.CopyContentUiToExport(state.value.contentToExport)) + } +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStrings.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStrings.kt new file mode 100644 index 000000000..a3926964a --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentStrings.kt @@ -0,0 +1,45 @@ +package br.alexandregpereira.hunter.shareContent.state + +import br.alexandregpereira.hunter.localization.Language + +interface ShareContentStrings { + val importButton: String + val copyButton: String + val copiedButton: String + val contentToImportLabel: String + val importTitle: String + val exportTitle: String + val importInvalidContentErrorMessage: String + val pasteContent: String +} + +internal data class ShareContentEnStrings( + override val importButton: String = "Import", + override val copyButton: String = "Copy", + override val copiedButton: String = "Copied", + override val contentToImportLabel: String = "Content", + override val importTitle: String = "Import Content", + override val exportTitle: String = "Share Content", + override val importInvalidContentErrorMessage: String = "Invalid content", + override val pasteContent: String = "Paste content", +) : ShareContentStrings + +internal data class ShareContentPtStrings( + override val importButton: String = "Importar", + override val copyButton: String = "Copiar", + override val copiedButton: String = "Copiado", + override val contentToImportLabel: String = "Conteúdo", + override val importTitle: String = "Importar Conteúdo", + override val exportTitle: String = "Compartilhar Conteúdo", + override val importInvalidContentErrorMessage: String = "Conteúdo inválido", + override val pasteContent: String = "Colar conteúdo", +) : ShareContentStrings + +fun ShareContentStrings(): ShareContentStrings = ShareContentEnStrings() + +internal fun Language.getStrings(): ShareContentStrings { + return when (this) { + Language.ENGLISH -> ShareContentEnStrings() + Language.PORTUGUESE -> ShareContentPtStrings() + } +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentUiEvent.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentUiEvent.kt new file mode 100644 index 000000000..09f26bd75 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/state/ShareContentUiEvent.kt @@ -0,0 +1,5 @@ +package br.alexandregpereira.hunter.shareContent.state + +internal sealed class ShareContentUiEvent { + data class CopyContentUiToExport(val content: String) : ShareContentUiEvent() +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentExportScreen.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentExportScreen.kt new file mode 100644 index 000000000..253616507 --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentExportScreen.kt @@ -0,0 +1,37 @@ +package br.alexandregpereira.hunter.shareContent.ui + +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 +import br.alexandregpereira.hunter.shareContent.state.ShareContentState +import br.alexandregpereira.hunter.ui.compose.AppButton +import br.alexandregpereira.hunter.ui.compose.AppTextField +import br.alexandregpereira.hunter.ui.compose.ScreenHeader + +@Composable +internal fun ShareContentExportScreen( + state: ShareContentState, + onCopy: () -> Unit, +) { + Spacer(modifier = Modifier.height(16.dp)) + + ScreenHeader(title = state.strings.exportTitle) + + Spacer(modifier = Modifier.height(16.dp)) + + AppTextField( + text = state.contentToExportShort, + label = state.strings.contentToImportLabel, + enabled = false, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + AppButton( + text = state.exportCopyButtonText, + enabled = state.exportCopyButtonEnabled, + onClick = onCopy, + ) +} diff --git a/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentImportScreen.kt b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentImportScreen.kt new file mode 100644 index 000000000..37506e27b --- /dev/null +++ b/feature/share-content/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/shareContent/ui/ShareContentImportScreen.kt @@ -0,0 +1,86 @@ +package br.alexandregpereira.hunter.shareContent.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.alexandregpereira.hunter.shareContent.state.ShareContentState +import br.alexandregpereira.hunter.shareContent.ui.resources.IconContentPaste +import br.alexandregpereira.hunter.shareContent.ui.resources.Res +import br.alexandregpereira.hunter.ui.compose.AppButton +import br.alexandregpereira.hunter.ui.compose.AppTextField +import br.alexandregpereira.hunter.ui.compose.ScreenHeader +import org.jetbrains.compose.resources.painterResource + +@Composable +internal fun ShareContentImportScreen( + state: ShareContentState, + onImport: () -> Unit, + onPaste: (String) -> Unit, +) { + Spacer(modifier = Modifier.height(16.dp)) + + ScreenHeader(title = state.strings.importTitle) + + Spacer(modifier = Modifier.height(16.dp)) + + AppTextField( + text = state.contentToImportShort, + label = state.strings.contentToImportLabel, + onValueChange = onPaste, + enabled = false, + showClearIcon = true, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val clipboardManager = LocalClipboardManager.current + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.clip(RoundedCornerShape(8.dp)).clickable { + onPaste(clipboardManager.getText()?.text.orEmpty()) + } + ) { + Icon( + painterResource(Res.drawable.IconContentPaste), + contentDescription = state.strings.pasteContent, + modifier = Modifier.padding(vertical = 8.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = state.strings.pasteContent, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + modifier = Modifier.padding(vertical = 8.dp), + ) + } + + state.importErrorMessage.takeIf { it.isNotBlank() }?.let { errorMessage -> + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = errorMessage, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + AppButton( + text = state.strings.importButton, + onClick = onImport, + ) +} diff --git a/feature/share-content/event/build.gradle.kts b/feature/share-content/event/build.gradle.kts new file mode 100644 index 000000000..d162a376c --- /dev/null +++ b/feature/share-content/event/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + kotlin("multiplatform") +} + +multiplatform { + commonMain { + implementation(project(":core:event")) + implementation(libs.koin.core) + implementation(libs.kotlin.coroutines.core) + } + jvmMain() + iosMain() +} diff --git a/feature/share-content/event/src/commonMain/kotlin/br/alexadregpereira/hunter/shareContent/event/ShareContentEventDispatcher.kt b/feature/share-content/event/src/commonMain/kotlin/br/alexadregpereira/hunter/shareContent/event/ShareContentEventDispatcher.kt new file mode 100644 index 000000000..99865f958 --- /dev/null +++ b/feature/share-content/event/src/commonMain/kotlin/br/alexadregpereira/hunter/shareContent/event/ShareContentEventDispatcher.kt @@ -0,0 +1,27 @@ +package br.alexadregpereira.hunter.shareContent.event + +import br.alexandregpereira.hunter.event.v2.EventDispatcher +import br.alexandregpereira.hunter.event.v2.EventListener +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance + +class ShareContentEventDispatcher : EventDispatcher by EventDispatcher() + +sealed class ShareContentEvent { + sealed class Import : ShareContentEvent() { + data object OnStart : Import() + data object OnFinish : Import() + } + sealed class Export : ShareContentEvent() { + data class OnStart(val monsterIndex: String) : Export() + data object OnFinish : Export() + } +} + +fun EventListener.importEvents(): Flow { + return events.filterIsInstance() +} + +fun EventListener.exportEvents(): Flow { + return events.filterIsInstance() +} diff --git a/settings.gradle b/settings.gradle index f9e3ff73b..14cee64db 100644 --- a/settings.gradle +++ b/settings.gradle @@ -69,6 +69,8 @@ include ':feature:search:compose' include ':feature:settings:compose' include ':feature:spell-detail:compose' include ':feature:spell-detail:event' +include ':feature:share-content:compose' +include ':feature:share-content:event' include ':feature:spell-compendium:compose' include ':feature:spell-compendium:event' include ':feature:spell-compendium:state-holder' diff --git a/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/AppTextField.kt b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/AppTextField.kt index 2c5321b3e..033feddd5 100644 --- a/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/AppTextField.kt +++ b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/AppTextField.kt @@ -46,10 +46,11 @@ fun AppTextField( multiline: Boolean = false, capitalize: Boolean = true, enabled: Boolean = true, + showClearIcon: Boolean = enabled, onValueChange: (String) -> Unit = {}, trailingIcon: @Composable (() -> Unit)? = { AnimatedVisibility( - visible = text.isNotEmpty() && enabled, + visible = text.isNotEmpty() && showClearIcon, enter = fadeIn(), exit = fadeOut(), ) { diff --git a/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/BottomSheet.kt b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/BottomSheet.kt index 27d8c49c5..617109692 100644 --- a/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/BottomSheet.kt +++ b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/BottomSheet.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp @Composable fun BottomSheet( + modifier: Modifier = Modifier, opened: Boolean = false, backgroundColor: Color = MaterialTheme.colors.background.copy(alpha = 0.3f), contentPadding: PaddingValues = PaddingValues(), @@ -74,7 +75,7 @@ fun BottomSheet( modifier = Modifier.fillMaxWidth() ) { Column( - modifier = Modifier.padding( + modifier = modifier.padding( start = contentPadding.calculateStartPadding(LayoutDirection.Ltr), end = contentPadding.calculateEndPadding(LayoutDirection.Ltr) )