From cdd2cb4407c4917d94330694de4415b8c9a8c035 Mon Sep 17 00:00:00 2001 From: Alexandre G Pereira Date: Sun, 21 Jul 2024 15:36:56 -0300 Subject: [PATCH] Add close screen button (#304) --- app/build.gradle.kts | 5 +- .../hunter/app/KoinTestRunner.kt | 48 ++++++++++ .../hunter/app/folder/FolderListTest.kt | 4 +- .../hunter/app/HunterApplication.kt | 21 +++-- .../hunter/app/di/AppModule.kt | 1 + app/src/jvmMain/kotlin/main.kt | 2 +- .../hunter/localization/Localization.kt | 14 +-- .../localization/di/LocalizationModule.kt | 11 ++- .../hunter/data/di/DatabaseModule.kt | 6 +- .../hunter/data/settings/di/DataModule.kt | 2 +- .../hunter/domain/sync/IsFirstTimeUseCase.kt | 28 ++++++ .../hunter/domain/sync/SyncUseCase.kt | 7 +- .../hunter/domain/sync/di/DomainModule.kt | 6 +- .../folder/detail/ui/FolderDetailScreen.kt | 42 ++++----- .../hunter/folder/list/ui/ItemSelection.kt | 45 ++++----- .../state-holder/build.gradle.kts | 1 + .../state/MonsterCompendiumStateHolder.kt | 7 ++ .../monster/compendium/state/di/Module.kt | 1 + .../MonsterCompendiumStateHolderTest.kt | 5 +- .../preview/ui/MonsterContentPreviewScreen.kt | 10 +- .../content/ui/MonsterContentManagerScreen.kt | 75 +++++++-------- .../hunter/detail/MonsterDetailFeature.kt | 16 ++-- .../hunter/detail/ui/MonsterDetailScreen.kt | 20 ++-- .../detail/MonsterDetailStateHolder.kt | 4 +- .../ui/MonsterRegistrationScreen.kt | 49 ++++------ gradle/libs.versions.toml | 8 +- .../hunter/ui/compose/Screen.kt | 91 +++++++++++++++++++ 27 files changed, 353 insertions(+), 176 deletions(-) create mode 100644 app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/KoinTestRunner.kt create mode 100644 domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/IsFirstTimeUseCase.kt create mode 100644 ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/Screen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6aaf0726..b89aa07ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,6 +112,9 @@ kotlin { dependencies { implementation(libs.bundles.instrumentedtest) implementation(libs.compose.ui.test) + implementation(libs.sqldelight.android) + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.test) debugImplementation(libs.compose.ui.test.manifest) } } @@ -173,7 +176,7 @@ android { setProperty("archivesBaseName", "app-dev") } - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "br.alexandregpereira.hunter.app.KoinTestRunner" } signingConfigs { diff --git a/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/KoinTestRunner.kt b/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/KoinTestRunner.kt new file mode 100644 index 000000000..922680d3d --- /dev/null +++ b/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/KoinTestRunner.kt @@ -0,0 +1,48 @@ +package br.alexandregpereira.hunter.app + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import br.alexandregpereira.hunter.data.Database +import br.alexandregpereira.hunter.localization.Language +import com.russhwolf.settings.MapSettings +import com.russhwolf.settings.Settings +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver +import org.koin.core.context.startKoin +import org.koin.dsl.module + +internal class KoinTestRunner : AndroidJUnitRunner() { + + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, KoinTestApplication::class.qualifiedName, context) + } +} + +internal class KoinTestApplication : Application() { + + override fun onCreate() { + super.onCreate() + startKoin { + initAndroidModules(this@KoinTestApplication) + allowOverride(true) + module { + factory { + AndroidSqliteDriver( + schema = Database.Schema, + context = get(), + name = null + ) + } + single { MapSettings() } + factory { Language.ENGLISH } + }.also { + modules(it) + } + } + } +} diff --git a/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/folder/FolderListTest.kt b/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/folder/FolderListTest.kt index 8a9b633bf..523179998 100644 --- a/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/folder/FolderListTest.kt +++ b/app/src/androidInstrumentedTest/kotlin/br/alexandregpereira/hunter/app/folder/FolderListTest.kt @@ -9,7 +9,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTouchInput -import androidx.test.espresso.Espresso import br.alexandregpereira.hunter.app.HunterApp import org.junit.Rule import org.junit.Test @@ -36,8 +35,7 @@ class FolderListTest { composeTestRule.onNodeWithText("Add to Folder").performClick() composeTestRule.onNodeWithText("Folder name").performTextInput("Folder Test") composeTestRule.onNodeWithText("Save").performClick() - Espresso.closeSoftKeyboard() - Espresso.pressBack() + composeTestRule.onNodeWithContentDescription("Close").performClick() composeTestRule.waitUntil(timeoutMillis = 5000) { composeTestRule.onNodeWithText("Folders").isDisplayed() } diff --git a/app/src/androidMain/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt b/app/src/androidMain/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt index 3e15c8a06..10c395758 100644 --- a/app/src/androidMain/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt +++ b/app/src/androidMain/kotlin/br/alexandregpereira/hunter/app/HunterApplication.kt @@ -22,6 +22,7 @@ import com.google.firebase.analytics.ktx.analytics import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import org.koin.android.ext.koin.androidContext +import org.koin.core.KoinApplication import org.koin.core.context.startKoin import org.koin.dsl.module @@ -34,14 +35,18 @@ class HunterApplication : Application() { private fun initKoin() { startKoin { - androidContext(this@HunterApplication) - modules( - module { - factory { Firebase.analytics } - factory { Firebase.crashlytics } - } - ) - initKoinModules() + initAndroidModules(this@HunterApplication) } } } + +internal fun KoinApplication.initAndroidModules(app: Application) { + androidContext(app) + modules( + module { + factory { Firebase.analytics } + factory { Firebase.crashlytics } + } + ) + initKoinModules() +} 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 10e447b1d..1d3a7274e 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 @@ -31,6 +31,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.module internal fun KoinApplication.initKoinModules() { + allowOverride(false) modules(domainModules) modules(dataModules) modules( diff --git a/app/src/jvmMain/kotlin/main.kt b/app/src/jvmMain/kotlin/main.kt index 8e195c0b1..dce62f7b7 100644 --- a/app/src/jvmMain/kotlin/main.kt +++ b/app/src/jvmMain/kotlin/main.kt @@ -24,7 +24,7 @@ fun main() = application { onCloseRequest = ::exitApplication, title = "Monster Compendium", state = rememberWindowState( - size = DpSize(520.dp, 920.dp) + size = DpSize(600.dp, 800.dp) ), onKeyEvent = onKeyEvent@ { keyEvent -> val keyPressedHandled = keyEvent.asKeyPressedHandled() ?: return@onKeyEvent false diff --git a/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt index 394e9a0b0..206876c9a 100644 --- a/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt +++ b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/Localization.kt @@ -20,9 +20,11 @@ interface MutableAppLocalization : AppLocalization { fun setLanguage(language: String) } -internal class AppLocalizationImpl : MutableAppLocalization, AppReactiveLocalization { +internal class AppLocalizationImpl( + private val defaultLanguage: Language, +) : MutableAppLocalization, AppReactiveLocalization { - private var language: Language = getDefaultLanguage() + private var language: Language = defaultLanguage private val _languageFlow: MutableSharedFlow = MutableSharedFlow( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, @@ -34,15 +36,9 @@ internal class AppLocalizationImpl : MutableAppLocalization, AppReactiveLocaliza } override fun setLanguage(language: String) { - this.language = Language.entries.firstOrNull { it.code == language } ?: getDefaultLanguage() + this.language = Language.entries.firstOrNull { it.code == language } ?: defaultLanguage _languageFlow.tryEmit(this.language) } - - private fun getDefaultLanguage(): Language { - return Language.entries.firstOrNull { - it.code == getDeviceLangCode() - } ?: Language.ENGLISH - } } internal expect fun getDeviceLangCode(): String diff --git a/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt index 79dc89642..0687bfe79 100644 --- a/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt +++ b/core/localization/src/commonMain/kotlin/br/alexandregpereira/hunter/localization/di/LocalizationModule.kt @@ -3,12 +3,21 @@ package br.alexandregpereira.hunter.localization.di import br.alexandregpereira.hunter.localization.AppLocalization import br.alexandregpereira.hunter.localization.AppLocalizationImpl import br.alexandregpereira.hunter.localization.AppReactiveLocalization +import br.alexandregpereira.hunter.localization.Language import br.alexandregpereira.hunter.localization.MutableAppLocalization +import br.alexandregpereira.hunter.localization.getDeviceLangCode import org.koin.dsl.module val localizationModule = module { - single { AppLocalizationImpl() } + factory { getDefaultLanguage() } + single { AppLocalizationImpl(get()) } single { get() } single { get() } single { get() } } + +private fun getDefaultLanguage(): Language { + return Language.entries.firstOrNull { + it.code == getDeviceLangCode() + } ?: Language.ENGLISH +} diff --git a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/di/DatabaseModule.kt b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/di/DatabaseModule.kt index dcf9ecf5f..4627ed3d1 100644 --- a/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/di/DatabaseModule.kt +++ b/domain/app/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/di/DatabaseModule.kt @@ -27,12 +27,16 @@ import br.alexandregpereira.hunter.data.monster.local.dao.MonsterDao import br.alexandregpereira.hunter.data.monster.lore.local.dao.MonsterLoreDao import br.alexandregpereira.hunter.data.source.local.dao.AlternativeSourceDao import br.alexandregpereira.hunter.data.spell.local.dao.SpellDao +import com.squareup.sqldelight.db.SqlDriver import kotlinx.coroutines.CoroutineDispatcher import org.koin.dsl.module val databaseModule = module { + factory { + createSqlDriver() + } single { - Database(createSqlDriver()) + Database(get()) } factory { AlternativeSourceDaoImpl( diff --git a/domain/settings/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/settings/di/DataModule.kt b/domain/settings/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/settings/di/DataModule.kt index 5bf8a0f8a..2442c5461 100644 --- a/domain/settings/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/settings/di/DataModule.kt +++ b/domain/settings/data/src/commonMain/kotlin/br/alexandregpereira/hunter/data/settings/di/DataModule.kt @@ -25,7 +25,7 @@ import org.koin.dsl.module val settingsDataModule = module { factory { AlternativeSourceUrlBuilder(get()) } - single { get().create("preferences") } + single { get().create("preferences") } factory { DefaultSettingsRepository(get()) } getAdditionalSettingsModule() } diff --git a/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/IsFirstTimeUseCase.kt b/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/IsFirstTimeUseCase.kt new file mode 100644 index 000000000..232960a2a --- /dev/null +++ b/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/IsFirstTimeUseCase.kt @@ -0,0 +1,28 @@ +package br.alexandregpereira.hunter.domain.sync + +import br.alexandregpereira.hunter.domain.settings.SettingsRepository +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.single + +fun interface IsFirstTime { + suspend operator fun invoke(): Boolean +} + +fun interface ResetFirstTime { + suspend operator fun invoke() +} + +private const val IsFirstTimeKey = "isFirstTimeKey" + + +internal fun IsFirstTime( + repository: SettingsRepository, +): IsFirstTime = IsFirstTime { + repository.getInt(key = IsFirstTimeKey).map { it ?: 1 }.map { it == 1 }.single() +} + +internal fun ResetFirstTime( + repository: SettingsRepository, +): ResetFirstTime = ResetFirstTime { + repository.saveInt(key = IsFirstTimeKey, value = 0).single() +} diff --git a/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/SyncUseCase.kt b/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/SyncUseCase.kt index 02467e489..a6e84fa6d 100644 --- a/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/SyncUseCase.kt +++ b/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/SyncUseCase.kt @@ -19,7 +19,6 @@ package br.alexandregpereira.hunter.domain.sync import br.alexandregpereira.hunter.domain.monster.lore.SyncMonstersLoreUseCase import br.alexandregpereira.hunter.domain.settings.GetContentVersionUseCase import br.alexandregpereira.hunter.domain.settings.GetLanguageUseCase -import br.alexandregpereira.hunter.domain.settings.IsLanguageSupported import br.alexandregpereira.hunter.domain.settings.SaveContentVersionUseCase import br.alexandregpereira.hunter.domain.settings.SaveLanguageUseCase import br.alexandregpereira.hunter.domain.spell.SyncSpellsUseCase @@ -46,6 +45,8 @@ class SyncUseCase internal constructor( private val saveLanguageUseCase: SaveLanguageUseCase, private val getContentVersionUseCase: GetContentVersionUseCase, private val saveContentVersionUseCase: SaveContentVersionUseCase, + private val isFirstTime: IsFirstTime, + private val resetFirstTime: ResetFirstTime, ) { private val contentVersion = 3 @@ -67,6 +68,7 @@ class SyncUseCase internal constructor( runCatching { saveContentVersionUseCase(lastContentVersionRollback).single() } } } + resetFirstTime() emit(SyncStatus.SYNCED) } else { emit(SyncStatus.IDLE) @@ -77,7 +79,8 @@ class SyncUseCase internal constructor( private fun isToSync(): Flow> { return isLangSyncScenario() .zip(isContentVersionSyncScenario()) { (isLangSyncScenario, lastLanguageRollback), (isContentVersionSyncScenario, lastContentVersionRollback) -> - Triple((isLangSyncScenario || isContentVersionSyncScenario), lastLanguageRollback, lastContentVersionRollback) + val isToSync = isFirstTime() || isLangSyncScenario || isContentVersionSyncScenario + Triple(isToSync, lastLanguageRollback, lastContentVersionRollback) } } diff --git a/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/di/DomainModule.kt b/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/di/DomainModule.kt index 06ee03b62..d820193c1 100644 --- a/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/di/DomainModule.kt +++ b/domain/sync/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/sync/di/DomainModule.kt @@ -16,9 +16,13 @@ package br.alexandregpereira.hunter.domain.sync.di +import br.alexandregpereira.hunter.domain.sync.IsFirstTime +import br.alexandregpereira.hunter.domain.sync.ResetFirstTime import br.alexandregpereira.hunter.domain.sync.SyncUseCase import org.koin.dsl.module val syncDomainModule = module { - factory { SyncUseCase(get(), get(), get(), get(), get(), get(), get()) } + factory { IsFirstTime(get()) } + factory { ResetFirstTime(get()) } + factory { SyncUseCase(get(), get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/feature/folder-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/detail/ui/FolderDetailScreen.kt b/feature/folder-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/detail/ui/FolderDetailScreen.kt index c1dc15154..ed850384c 100644 --- a/feature/folder-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/detail/ui/FolderDetailScreen.kt +++ b/feature/folder-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/detail/ui/FolderDetailScreen.kt @@ -17,10 +17,8 @@ package br.alexandregpereira.hunter.folder.detail.ui import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import br.alexandregpereira.hunter.domain.folder.model.MonsterPreviewFolder import br.alexandregpereira.hunter.folder.detail.FolderDetailState import br.alexandregpereira.hunter.ui.compendium.CompendiumItemState.Item @@ -30,9 +28,7 @@ import br.alexandregpereira.hunter.ui.compendium.monster.MonsterCardState import br.alexandregpereira.hunter.ui.compendium.monster.MonsterCompendium import br.alexandregpereira.hunter.ui.compendium.monster.MonsterImageState import br.alexandregpereira.hunter.ui.compendium.monster.MonsterTypeState -import br.alexandregpereira.hunter.ui.compose.BackHandler -import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss -import br.alexandregpereira.hunter.ui.compose.Window +import br.alexandregpereira.hunter.ui.compose.AppFullScreen @Composable internal fun FolderDetailScreen( @@ -41,29 +37,23 @@ internal fun FolderDetailScreen( onItemCLick: (index: String) -> Unit = {}, onItemLongCLick: (index: String) -> Unit = {}, onClose: () -> Unit = {} -) { - BackHandler(enabled = state.isOpen, onBack = onClose) - +) = AppFullScreen(isOpen = state.isOpen, contentPadding, onClose = onClose) { val monsters = remember(state.monsters) { state.monsters.asState() } - SwipeVerticalToDismiss(visible = state.isOpen, onClose = onClose) { - Window(Modifier.fillMaxSize()) { - val items = listOf( - Title( - value = state.folderName, - isHeader = true - ), - ) + monsters.map { - Item(value = it) - } - MonsterCompendium( - items = items, - animateItems = true, - contentPadding = contentPadding, - onItemCLick = onItemCLick, - onItemLongCLick = onItemLongCLick - ) - } + val items = listOf( + Title( + value = state.folderName, + isHeader = true + ), + ) + monsters.map { + Item(value = it) } + MonsterCompendium( + items = items, + animateItems = true, + contentPadding = contentPadding, + onItemCLick = onItemCLick, + onItemLongCLick = onItemLongCLick + ) } private fun List.asState(): List = map { diff --git a/feature/folder-list/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/list/ui/ItemSelection.kt b/feature/folder-list/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/list/ui/ItemSelection.kt index 630f9199a..a5a15c648 100644 --- a/feature/folder-list/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/list/ui/ItemSelection.kt +++ b/feature/folder-list/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/folder/list/ui/ItemSelection.kt @@ -34,9 +34,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import br.alexandregpereira.hunter.ui.compose.AppButton -import br.alexandregpereira.hunter.ui.compose.BackHandler +import br.alexandregpereira.hunter.ui.compose.AppScreen import br.alexandregpereira.hunter.ui.compose.ScreenHeader -import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss import br.alexandregpereira.hunter.ui.theme.HunterTheme import org.jetbrains.compose.ui.tooling.preview.Preview @@ -49,33 +48,29 @@ internal fun BoxScope.ItemSelection( contentBottomPadding: Dp = 0.dp, onClose: () -> Unit = {}, onDeleteClick: () -> Unit = {} +) = AppScreen( + isOpen = isOpen, + onClose = onClose, + modifier = Modifier.align(BottomCenter) ) { - BackHandler(enabled = isOpen, onBack = onClose) - - SwipeVerticalToDismiss( - visible = isOpen, - onClose = onClose, - modifier = Modifier.align(BottomCenter) + Card( + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + modifier = modifier + .fillMaxWidth() + .verticalScroll(state = rememberScrollState()) ) { - Card( - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - modifier = modifier - .fillMaxWidth() - .verticalScroll(state = rememberScrollState()) - ) { - Column(Modifier.padding(16.dp)) { - ScreenHeader( - title = itemSelectionText, - ) + Column(Modifier.padding(16.dp)) { + ScreenHeader( + title = itemSelectionText, + ) - AppButton( - text = deleteText, - modifier = Modifier.padding(top = 24.dp), - onClick = onDeleteClick - ) + AppButton( + text = deleteText, + modifier = Modifier.padding(top = 24.dp), + onClick = onDeleteClick + ) - Spacer(modifier = Modifier.height(contentBottomPadding)) - } + Spacer(modifier = Modifier.height(contentBottomPadding)) } } } diff --git a/feature/monster-compendium/state-holder/build.gradle.kts b/feature/monster-compendium/state-holder/build.gradle.kts index 039d2dc9a..0ffa658a0 100644 --- a/feature/monster-compendium/state-holder/build.gradle.kts +++ b/feature/monster-compendium/state-holder/build.gradle.kts @@ -11,6 +11,7 @@ multiplatform { api(project(":core:ui:state-recovery")) implementation(project(":domain:monster:event")) api(project(":domain:monster-compendium:core")) + implementation(project(":domain:sync:core")) implementation(project(":feature:folder-preview:event")) implementation(project(":feature:sync:event")) implementation(project(":feature:monster-registration:event")) diff --git a/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt b/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt index ed73560fb..5fcbf31bc 100644 --- a/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt +++ b/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt @@ -16,6 +16,7 @@ package br.alexandregpereira.hunter.monster.compendium.state +import br.alexandregpereira.hunter.domain.sync.IsFirstTime import br.alexandregpereira.hunter.domain.usecase.GetLastCompendiumScrollItemPositionUseCase import br.alexandregpereira.hunter.domain.usecase.SaveCompendiumScrollItemPositionUseCase import br.alexandregpereira.hunter.event.EventListener @@ -67,6 +68,7 @@ class MonsterCompendiumStateHolder internal constructor( private val dispatcher: CoroutineDispatcher, private val analytics: MonsterCompendiumAnalytics, private val stateRecovery: StateRecovery, + private val isFirstTime: IsFirstTime, appLocalization: AppLocalization, ) : UiModel( initialState = MonsterCompendiumState(strings = appLocalization.getStrings()), @@ -88,6 +90,11 @@ class MonsterCompendiumStateHolder internal constructor( } private suspend fun fetchMonsterCompendium() { + if (isFirstTime()) { + setState { loading(isLoading = true) } + return + } + getMonsterCompendiumUseCase() .zip( getLastCompendiumScrollItemPositionUseCase() diff --git a/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/di/Module.kt b/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/di/Module.kt index 3a5980c6f..9e8c19db3 100644 --- a/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/di/Module.kt +++ b/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/di/Module.kt @@ -44,6 +44,7 @@ val monsterCompendiumModule = module { analytics = MonsterCompendiumAnalytics(get()), appLocalization = get(), stateRecovery = get(named(StateRecoveryQualifier)), + isFirstTime = get(), ) } } diff --git a/feature/monster-compendium/state-holder/src/commonTest/kotlin/br/alexandregpereira/hunter/monster/compendium/MonsterCompendiumStateHolderTest.kt b/feature/monster-compendium/state-holder/src/commonTest/kotlin/br/alexandregpereira/hunter/monster/compendium/MonsterCompendiumStateHolderTest.kt index 708763dc5..beec2c0a0 100644 --- a/feature/monster-compendium/state-holder/src/commonTest/kotlin/br/alexandregpereira/hunter/monster/compendium/MonsterCompendiumStateHolderTest.kt +++ b/feature/monster-compendium/state-holder/src/commonTest/kotlin/br/alexandregpereira/hunter/monster/compendium/MonsterCompendiumStateHolderTest.kt @@ -48,9 +48,7 @@ import br.alexandregpereira.hunter.monster.event.emptyMonsterEventDispatcher import br.alexandregpereira.hunter.monster.registration.event.MonsterRegistrationEventListener import br.alexandregpereira.hunter.monster.registration.event.emptyMonsterRegistrationEventListener import br.alexandregpereira.hunter.sync.event.SyncEventDispatcher -import br.alexandregpereira.hunter.sync.event.SyncEventListener import br.alexandregpereira.hunter.sync.event.emptySyncEventDispatcher -import br.alexandregpereira.hunter.sync.event.emptySyncEventListener import br.alexandregpereira.hunter.ui.StateRecovery import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -367,7 +365,8 @@ class MonsterCompendiumStateHolderTest { appLocalization = object : AppLocalization { override fun getLanguage(): Language = Language.ENGLISH }, - stateRecovery = StateRecovery() + stateRecovery = StateRecovery(), + isFirstTime = { false }, ) } diff --git a/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/preview/ui/MonsterContentPreviewScreen.kt b/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/preview/ui/MonsterContentPreviewScreen.kt index 2777b5165..2d8c20f50 100644 --- a/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/preview/ui/MonsterContentPreviewScreen.kt +++ b/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/preview/ui/MonsterContentPreviewScreen.kt @@ -50,15 +50,13 @@ import br.alexandregpereira.hunter.ui.compendium.monster.MonsterCardState import br.alexandregpereira.hunter.ui.compendium.monster.MonsterCompendium import br.alexandregpereira.hunter.ui.compendium.monster.MonsterImageState import br.alexandregpereira.hunter.ui.compendium.monster.MonsterTypeState -import br.alexandregpereira.hunter.ui.compose.BackHandler +import br.alexandregpereira.hunter.ui.compose.AppScreen import br.alexandregpereira.hunter.ui.compose.Closeable import br.alexandregpereira.hunter.ui.compose.LoadingScreen import br.alexandregpereira.hunter.ui.compose.PopupContainer -import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss import br.alexandregpereira.hunter.ui.compose.tablecontent.TableContentItemState import br.alexandregpereira.hunter.ui.compose.tablecontent.TableContentItemTypeState import br.alexandregpereira.hunter.ui.compose.tablecontent.TableContentPopup -import br.alexandregpereira.hunter.ui.theme.HunterTheme import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @@ -71,15 +69,13 @@ internal fun MonsterContentPreviewScreen( onTableContentClose: () -> Unit = {}, onTableContentClick: (Int) -> Unit = {}, onFirstVisibleItemChange: (Int) -> Unit = {}, -) = HunterTheme { - BackHandler(enabled = state.isOpen, onBack = onClose) - +) { Closeable( opened = state.isOpen, onClosed = onClose, ) - SwipeVerticalToDismiss(visible = state.isOpen, onClose = onClose) { + AppScreen(isOpen = state.isOpen, contentPadding, onClose = onClose) { Surface( Modifier .fillMaxSize() diff --git a/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/ui/MonsterContentManagerScreen.kt b/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/ui/MonsterContentManagerScreen.kt index 47278bcfe..9370e0f57 100644 --- a/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/ui/MonsterContentManagerScreen.kt +++ b/feature/monster-content-manager/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/content/ui/MonsterContentManagerScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import br.alexandregpereira.hunter.monster.content.MonsterContentManagerState import br.alexandregpereira.hunter.monster.content.MonsterContentState +import br.alexandregpereira.hunter.ui.compose.AppFullScreen import br.alexandregpereira.hunter.ui.compose.BackHandler import br.alexandregpereira.hunter.ui.compose.SectionTitle import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss @@ -41,48 +42,42 @@ internal fun MonsterContentManagerScreen( onAddClick: (String) -> Unit = {}, onRemoveClick: (String) -> Unit = {}, onPreviewClick: (String, String) -> Unit = { _, _ -> }, -) { - BackHandler(enabled = state.isOpen, onBack = onClose) - - SwipeVerticalToDismiss(visible = state.isOpen, onClose = onClose) { - Window(Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.padding(horizontal = 16.dp), - contentPadding = PaddingValues( - top = 24.dp + contentPadding.calculateTopPadding(), - bottom = contentPadding.calculateBottomPadding() - ) - ) { - item(key = "title") { - SectionTitle( - title = state.strings.title, - isHeader = true, - modifier = Modifier - .padding(bottom = 32.dp) - ) - } +) = AppFullScreen(isOpen = state.isOpen, contentPadding, onClose = onClose) { + LazyColumn( + modifier = Modifier.padding(horizontal = 16.dp), + contentPadding = PaddingValues( + top = 24.dp + contentPadding.calculateTopPadding(), + bottom = contentPadding.calculateBottomPadding() + ) + ) { + item(key = "title") { + SectionTitle( + title = state.strings.title, + isHeader = true, + modifier = Modifier + .padding(bottom = 32.dp) + ) + } - items(state.monsterContents, key = { it.acronym }) { monsterContent -> - MonsterContentCard( - name = monsterContent.name, - originalName = monsterContent.originalName, - totalMonsters = monsterContent.totalMonsters, - summary = monsterContent.summary, - coverImageUrl = monsterContent.coverImageUrl, - isEnabled = monsterContent.isEnabled, - strings = state.strings, - onAddClick = { onAddClick(monsterContent.acronym) }, - onRemoveClick = { onRemoveClick(monsterContent.acronym) }, - onPreviewClick = { - onPreviewClick( - monsterContent.acronym, - monsterContent.name - ) - }, + items(state.monsterContents, key = { it.acronym }) { monsterContent -> + MonsterContentCard( + name = monsterContent.name, + originalName = monsterContent.originalName, + totalMonsters = monsterContent.totalMonsters, + summary = monsterContent.summary, + coverImageUrl = monsterContent.coverImageUrl, + isEnabled = monsterContent.isEnabled, + strings = state.strings, + onAddClick = { onAddClick(monsterContent.acronym) }, + onRemoveClick = { onRemoveClick(monsterContent.acronym) }, + onPreviewClick = { + onPreviewClick( + monsterContent.acronym, + monsterContent.name ) - Spacer(modifier = Modifier.padding(bottom = 48.dp)) - } - } + }, + ) + Spacer(modifier = Modifier.padding(bottom = 48.dp)) } } } 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 3eab4aced..7003489e1 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 @@ -32,6 +32,7 @@ import br.alexandregpereira.hunter.detail.ui.MonsterDetailScreen import br.alexandregpereira.hunter.detail.ui.strings import br.alexandregpereira.hunter.monster.detail.MonsterDetailStateHolder import br.alexandregpereira.hunter.monster.detail.di.MonsterDetailStateRecoveryQualifier +import br.alexandregpereira.hunter.ui.compose.AppScreen import br.alexandregpereira.hunter.ui.compose.BackHandler import br.alexandregpereira.hunter.ui.compose.ConfirmationBottomSheet import br.alexandregpereira.hunter.ui.compose.FormBottomSheet @@ -41,7 +42,6 @@ import br.alexandregpereira.hunter.ui.compose.LocalScreenSize import br.alexandregpereira.hunter.ui.compose.StateRecoveryLaunchedEffect import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss import br.alexandregpereira.hunter.ui.compose.getPlatformScreenSizeInfo -import br.alexandregpereira.hunter.ui.theme.HunterTheme import br.alexandregpereira.hunter.ui.theme.Shapes import org.koin.compose.koinInject import org.koin.core.qualifier.named @@ -49,7 +49,7 @@ import org.koin.core.qualifier.named @Composable fun MonsterDetailFeature( contentPadding: PaddingValues = PaddingValues(0.dp), -) = HunterTheme { +) { StateRecoveryLaunchedEffect( key = MonsterDetailStateRecoveryQualifier, stateRecovery = koinInject(named(MonsterDetailStateRecoveryQualifier)), @@ -58,11 +58,12 @@ fun MonsterDetailFeature( val viewModel: MonsterDetailStateHolder = koinInject() val viewState by viewModel.state.collectAsState() - BackHandler(enabled = viewState.showDetail) { - viewModel.onClose() - } - - SwipeVerticalToDismiss(visible = viewState.showDetail, onClose = viewModel::onClose) { + AppScreen( + isOpen = viewState.showDetail, + contentPaddingValues = contentPadding, + closeable = false, + onClose = viewModel::onClose + ) { LoadingScreen( isLoading = viewState.isLoading, showCircularLoading = false, @@ -85,6 +86,7 @@ fun MonsterDetailFeature( onOptionsClicked = viewModel::onShowOptionsClicked, onSpellClicked = viewModel::onSpellClicked, onLoreClicked = viewModel::onLoreClicked, + onClose = viewModel::onClose, ) MonsterDetailOptionPicker( diff --git a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt index e2e9d2eb2..f94b0be98 100644 --- a/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt +++ b/feature/monster-detail/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager @@ -59,7 +60,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.lerp import androidx.compose.ui.layout.Layout -import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -71,6 +71,7 @@ import br.alexandregpereira.hunter.monster.detail.MonsterState import br.alexandregpereira.hunter.monster.detail.SpeedState import br.alexandregpereira.hunter.monster.detail.StatsState import br.alexandregpereira.hunter.ui.compose.AppBarIcon +import br.alexandregpereira.hunter.ui.compose.BoxCloseButton import br.alexandregpereira.hunter.ui.compose.ChallengeRatingCircle import br.alexandregpereira.hunter.ui.compose.LocalScreenSize import br.alexandregpereira.hunter.ui.compose.MonsterTypeIcon @@ -83,6 +84,7 @@ import br.alexandregpereira.hunter.ui.transition.transitionHorizontalScrollable import br.alexandregpereira.hunter.ui.util.toColor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable internal fun MonsterDetailScreen( @@ -97,7 +99,8 @@ internal fun MonsterDetailScreen( onMonsterChanged: (monster: MonsterState) -> Unit = {}, onOptionsClicked: () -> Unit = {}, onSpellClicked: (String) -> Unit = {}, - onLoreClicked: (String) -> Unit = {} + onLoreClicked: (String) -> Unit = {}, + onClose: () -> Unit = {}, ) = Surface { HorizontalPagerTransitionController(pagerState) @@ -116,8 +119,8 @@ internal fun MonsterDetailScreen( ScrollableBackground( getScrollPositionOffset = { scrollState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == MONSTER_TITLE_ITEM_KEY } - ?.run { - offset.coerceAtLeast(0) + (size / 2) + ?.let { itemInfo: LazyListItemInfo -> + itemInfo.offset.coerceAtLeast(0) } ?: 0 } ) @@ -142,7 +145,7 @@ internal fun MonsterDetailScreen( } item(key = MONSTER_TITLE_ITEM_KEY) { - val shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + val shape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) } MonsterTitleCompose( monsterTitleStates = monsters.map { MonsterTitleState( @@ -172,6 +175,8 @@ internal fun MonsterDetailScreen( ) } + BoxCloseButton(onClick = onClose) + MonsterTopBar( monsters, pagerState, @@ -205,7 +210,10 @@ private fun ScrollableBackground( Box( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colors.surface) + .background( + shape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) }, + color = MaterialTheme.colors.surface + ) ) } ) { measurables, constraints -> 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 e29f35cfd..0dbe02679 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 @@ -125,13 +125,13 @@ class MonsterDetailStateHolder internal constructor( analytics.trackMonsterDetailShown(event) enableMonsterPageChangesEventDispatch = event.enableMonsterPageChangesEventDispatch - getMonstersByInitialIndex(event.index, event.indexes) setState { copy( showDetail = true, strings = appLocalization.getStrings() ).saveState(stateRecovery) } + getMonstersByInitialIndex(event.index, event.indexes) } Hide -> { @@ -155,6 +155,8 @@ class MonsterDetailStateHolder internal constructor( monsterIndexes: List, invalidateCache: Boolean = false ) { + if (state.value.showDetail.not()) return + stateRecovery.saveMonsterIndexes(monsterIndexes) onMonsterChanged(monsterIndex, scrolled = false) getMonsterDetail(invalidateCache = invalidateCache).collectDetail() diff --git a/feature/monster-registration/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt b/feature/monster-registration/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt index c640f8cb0..742faffe5 100644 --- a/feature/monster-registration/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt +++ b/feature/monster-registration/compose/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/ui/MonsterRegistrationScreen.kt @@ -1,7 +1,6 @@ package br.alexandregpereira.hunter.monster.registration.ui import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -12,9 +11,7 @@ import br.alexandregpereira.hunter.monster.registration.MonsterRegistrationActio import br.alexandregpereira.hunter.monster.registration.MonsterRegistrationIntent import br.alexandregpereira.hunter.monster.registration.MonsterRegistrationState import br.alexandregpereira.hunter.state.ActionHandler -import br.alexandregpereira.hunter.ui.compose.BackHandler -import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss -import br.alexandregpereira.hunter.ui.compose.Window +import br.alexandregpereira.hunter.ui.compose.AppFullScreen import kotlinx.coroutines.flow.collectLatest @Composable @@ -23,35 +20,29 @@ internal fun MonsterRegistrationScreen( actionHandler: ActionHandler, contentPadding: PaddingValues = PaddingValues(), intent: MonsterRegistrationIntent = EmptyMonsterRegistrationIntent(), -) { - BackHandler(enabled = state.isOpen, onBack = intent::onClose) +) = AppFullScreen(isOpen = state.isOpen, contentPadding, onClose = intent::onClose) { + CompositionLocalProvider(LocalStrings provides state.strings) { + val lazyListState = rememberLazyListState() - SwipeVerticalToDismiss(visible = state.isOpen, onClose = intent::onClose) { - Window(Modifier.fillMaxSize()) { - CompositionLocalProvider(LocalStrings provides state.strings) { - val lazyListState = rememberLazyListState() - - LaunchedEffect(actionHandler.action) { - actionHandler.action.collectLatest { action -> - when (action) { - is MonsterRegistrationAction.GoToListPosition -> { - lazyListState.animateScrollToItem(action.position) - } - } + LaunchedEffect(actionHandler.action) { + actionHandler.action.collectLatest { action -> + when (action) { + is MonsterRegistrationAction.GoToListPosition -> { + lazyListState.animateScrollToItem(action.position) } } - - MonsterRegistrationForm( - monster = state.monster, - lazyListState = lazyListState, - isSaveButtonEnabled = state.isSaveButtonEnabled, - tableContent = state.tableContent, - isTableContentOpen = state.isTableContentOpen, - modifier = Modifier, - contentPadding = contentPadding, - intent = intent, - ) } } + + MonsterRegistrationForm( + monster = state.monster, + lazyListState = lazyListState, + isSaveButtonEnabled = state.isSaveButtonEnabled, + tableContent = state.tableContent, + isTableContentOpen = state.isTableContentOpen, + modifier = Modifier, + contentPadding = contentPadding, + intent = intent, + ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43c177568..a78e383ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] android_gradle_plugin = "8.3.0" +android_test_version = "1.6.1" appcompat = "1.7.0" arch_core_testing = "2.2.0" coil3 = "3.0.0-alpha08" @@ -7,7 +8,6 @@ compose_activity = '1.9.0' compose = '1.6.8' compose_plugin = '1.6.11' core_ktx = '1.13.1' -espresso_core = "3.6.1" firebase_bom = "33.1.2" googleplay_services = "4.4.2" gradle_firebase_crashlytics = "3.0.2" @@ -27,6 +27,7 @@ multiplatform-settings = "1.1.1" sqldelight = "1.5.5" [libraries] +android-test-runner = { module = "androidx.test:runner", version.ref = "android_test_version" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" } coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil3" } @@ -38,7 +39,6 @@ compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", ve compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "core_ktx" } core-testing = { module = "androidx.arch.core:core-testing", version.ref = "arch_core_testing" } -espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso_core" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase_bom" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } @@ -46,7 +46,6 @@ gradle-android = { module = "com.android.tools.build:gradle", version.ref = "and gradle-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "gradle_firebase_crashlytics" } gradle-googleplay-services = { module = "com.google.gms:google-services", version.ref = "googleplay_services" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -gradle-sqldelight = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "sqldelight" } junit-ext = { module = "androidx.test.ext:junit", version.ref = "junit_ext" } junit = { module = "junit:junit", version.ref = "junit" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } @@ -67,12 +66,13 @@ ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } material = { module = "com.google.android.material:material", version.ref = "material" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } +multiplatform-settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatform-settings" } sqldelight-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-ios = { module = "com.squareup.sqldelight:native-driver", version.ref = "sqldelight" } sqldelight-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } [bundles] -instrumentedtest = ["espresso", "junit-ext"] +instrumentedtest = ["android-test-runner", "junit-ext"] unittest = ["kotlin-coroutines-test", "core-testing", "mockk", "junit", "kotlin-test"] [plugins] diff --git a/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/Screen.kt b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/Screen.kt new file mode 100644 index 000000000..b04b24f50 --- /dev/null +++ b/ui/core/src/commonMain/kotlin/br/alexandregpereira/hunter/ui/compose/Screen.kt @@ -0,0 +1,91 @@ +package br.alexandregpereira.hunter.ui.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp + +@Composable +fun AppScreen( + isOpen: Boolean, + contentPaddingValues: PaddingValues = PaddingValues(), + modifier: Modifier = Modifier, + closeable: Boolean = true, + onClose: () -> Unit, + content: @Composable () -> Unit +) { + BackHandler(enabled = isOpen, onBack = onClose) + + SwipeVerticalToDismiss(visible = isOpen, onClose = onClose, modifier = modifier) { + if (closeable) { + BoxClosable( + contentPaddingValues = contentPaddingValues, + onClick = onClose, + content = content, + ) + } else { + content() + } + } +} + +@Composable +fun AppFullScreen( + isOpen: Boolean, + contentPaddingValues: PaddingValues, + onClose: () -> Unit, + content: @Composable () -> Unit +) { + AppScreen(isOpen = isOpen, contentPaddingValues = contentPaddingValues, onClose = onClose) { + Window(Modifier.fillMaxSize()) { + BoxClosable( + contentPaddingValues = contentPaddingValues, + onClick = onClose, + content = content, + ) + } + } +} + +@Composable +fun BoxClosable( + contentPaddingValues: PaddingValues, + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) = Box(modifier) { + content() + BoxCloseButton( + onClick = onClick, + modifier = Modifier + .padding(top = contentPaddingValues.calculateTopPadding()) + ) +} + +@Composable +fun BoxCloseButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) = Box( + modifier = modifier.size(48.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = onClick + ) + .clip(CircleShape) + .semantics { contentDescription = "Close" } +)