diff --git a/apps/mobile-app/build.gradle.kts b/apps/mobile-app/build.gradle.kts index 3d4f1fe..d2d22c3 100644 --- a/apps/mobile-app/build.gradle.kts +++ b/apps/mobile-app/build.gradle.kts @@ -71,6 +71,9 @@ dependencies { implementation(composeBom) androidTestImplementation(composeBom) + val koinBom = platform(libs.koin.bom) + implementation(koinBom) + implementation(libs.androidx.activity.compose) implementation(libs.androidx.browser) implementation(libs.androidx.compose.ui) @@ -88,10 +91,13 @@ dependencies { implementation(libs.androidx.navigation.runtime.ktx) implementation(libs.androidx.window) implementation(libs.coil.compose) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.coroutines.core) + implementation(libs.google.guava) implementation(libs.google.oss.licenses) implementation(libs.jakewharton.timber) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.androidx.compose.navigation) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) implementation(libs.bundles.database.room) testImplementation(libs.junit) diff --git a/apps/mobile-app/src/main/AndroidManifest.xml b/apps/mobile-app/src/main/AndroidManifest.xml index 4f15c38..c2106ba 100644 --- a/apps/mobile-app/src/main/AndroidManifest.xml +++ b/apps/mobile-app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ + diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/CappajvApp.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/CappajvApp.kt index fda553d..ee94f3d 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/CappajvApp.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/CappajvApp.kt @@ -14,6 +14,11 @@ import coil.disk.DiskCache import coil.memory.MemoryCache import coil.request.CachePolicy import coil.util.DebugLogger +import dev.marlonlom.apps.cappajv.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.core.logger.Level import timber.log.Timber /** @@ -30,9 +35,11 @@ val Context.dataStore by preferencesDataStore("cappajv-preferences") * @author marlonlom */ class CappajvApp : Application(), ImageLoaderFactory { + override fun onCreate() { super.onCreate() setupTimber() + initializeKoinConfig() } override fun newImageLoader(): ImageLoader = ImageLoader(this) @@ -60,4 +67,12 @@ class CappajvApp : Application(), ImageLoaderFactory { } } + private fun initializeKoinConfig() { + startKoin { + androidContext(this@CappajvApp) + androidLogger(Level.DEBUG) + modules(appModule) + } + } + } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/app_module.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/app_module.kt new file mode 100644 index 0000000..f995a0c --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/app_module.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.di + +import dev.marlonlom.apps.cappajv.ui.main.MainActivityViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val mainActivityModule = module { + viewModelOf(::MainActivityViewModel) +} + +val appModule = module { + includes(viewModelsModule) + includes(mainActivityModule) +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/data_module.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/data_module.kt new file mode 100644 index 0000000..5368b8b --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/data_module.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.di + +import dev.marlonlom.apps.cappajv.core.catalog_source.CatalogDataService +import dev.marlonlom.apps.cappajv.core.database.CappaDatabase +import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSource +import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSourceImpl +import dev.marlonlom.apps.cappajv.core.preferences.UserPreferencesRepository +import dev.marlonlom.apps.cappajv.dataStore +import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListRepository +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module +import java.util.Locale + +val dataModule = module { + single { + CappaDatabase.getInstance(androidContext()).let { db -> + LocalDataSourceImpl( + catalogItemsDao = db.catalogProductsDao(), + catalogPunctuationsDao = db.catalogPunctuationsDao(), + catalogFavoriteItemsDao = db.catalogFavoriteItemsDao() + ) + } + } + single { + CatalogDataService(Locale.getDefault().language) + } + single { + CatalogListRepository( + localDataSource = get(), + catalogDataService = get(), + ) + } + single { + UserPreferencesRepository(androidContext().dataStore) + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/viewmodels_module.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/viewmodels_module.kt new file mode 100644 index 0000000..fc03fdf --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/di/viewmodels_module.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.di + +import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailViewModel +import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListViewModel +import dev.marlonlom.apps.cappajv.features.settings.SettingsViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val viewModelsModule = module { + includes(dataModule) + viewModelOf(::CatalogListViewModel) + viewModelOf(::CatalogDetailViewModel) + viewModelOf(::SettingsViewModel) +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt index 365c49b..b20dbb3 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt @@ -8,7 +8,6 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch @@ -34,15 +33,4 @@ class CatalogDetailViewModel( } } - companion object { - - fun factory( - repository: CatalogDetailRepository - ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return CatalogDetailViewModel(repository) as T - } - } - } } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_favorites/FavoriteProductsRoute.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_favorites/FavoriteProductsRoute.kt index e26c851..20da13e 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_favorites/FavoriteProductsRoute.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_favorites/FavoriteProductsRoute.kt @@ -21,8 +21,8 @@ fun FavoriteProductsRoute( appState: CappajvAppState, ) { val contentHorizontalPadding = when { - appState.isLandscapeOrientation.not().and(appState.is7InTabletWidth) -> 40.dp - appState.isLandscapeOrientation.not().and(appState.is10InTabletWidth) -> 80.dp + appState.isLandscape.not().and(appState.isMediumWidth) -> 40.dp + appState.isLandscape.not().and(appState.isExpandedWidth) -> 80.dp else -> 20.dp } Column( diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRoute.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRoute.kt index faaa792..76bbf24 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRoute.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRoute.kt @@ -27,6 +27,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -38,11 +39,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import coil.request.ImageRequest import dev.marlonlom.apps.cappajv.R import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItemTuple import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState +import org.koin.androidx.compose.koinViewModel import timber.log.Timber @ExperimentalFoundationApi @@ -51,10 +54,14 @@ import timber.log.Timber @Composable fun CatalogListRoute( appState: CappajvAppState, + viewModel: CatalogListViewModel = koinViewModel(), ) { + + val catalogListState: CatalogListState by viewModel.uiState.collectAsStateWithLifecycle() + val contentHorizontalPadding = when { - appState.isLandscapeOrientation.not().and(appState.is7InTabletWidth) -> 40.dp - appState.isLandscapeOrientation.not().and(appState.is10InTabletWidth) -> 80.dp + appState.isLandscape.not().and(appState.isMediumWidth) -> 40.dp + appState.isLandscape.not().and(appState.isExpandedWidth) -> 80.dp else -> 20.dp } @@ -76,7 +83,7 @@ fun CatalogListRoute( ) } - when (val catalogListState = appState.catalogListState) { + when (catalogListState) { CatalogListState.Empty -> { item { Text(" :( ") @@ -88,8 +95,9 @@ fun CatalogListRoute( } is CatalogListState.Listing -> { + val listingsData = catalogListState as CatalogListState.Listing - catalogListState.map.keys.sorted().forEach { category -> + listingsData.map.keys.sorted().forEach { category -> item { Row( modifier = Modifier @@ -110,7 +118,7 @@ fun CatalogListRoute( } item { - val tuples: List = catalogListState.map[category] + val tuples: List = listingsData.map[category] .orEmpty().shuffled().subList(0, 5) LazyRow( horizontalArrangement = Arrangement.spacedBy(20.dp), diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_search/SearchProductsRoute.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_search/SearchProductsRoute.kt index 6e7971d..b1f3ca7 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_search/SearchProductsRoute.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_search/SearchProductsRoute.kt @@ -21,8 +21,8 @@ fun SearchProductsRoute( appState: CappajvAppState, ) { val contentHorizontalPadding = when { - appState.isLandscapeOrientation.not().and(appState.is7InTabletWidth) -> 40.dp - appState.isLandscapeOrientation.not().and(appState.is10InTabletWidth) -> 80.dp + appState.isLandscape.not().and(appState.isMediumWidth) -> 40.dp + appState.isLandscape.not().and(appState.isExpandedWidth) -> 80.dp else -> 20.dp } Column( diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/viewmodel.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/SettingsViewModel.kt similarity index 100% rename from apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/viewmodel.kt rename to apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/SettingsViewModel.kt diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog.kt index 3c14004..e989f06 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,6 +25,7 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.marlonlom.apps.cappajv.R import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState +import org.koin.androidx.compose.koinViewModel /** * Settings dialog route composable ui. @@ -41,10 +42,10 @@ import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState @Composable fun SettingsDialog( appState: CappajvAppState, - viewModel: SettingsViewModel, onDialogDismissed: () -> Unit, openOssLicencesInfo: () -> Unit, - openExternalUrl: (String) -> Unit + openExternalUrl: (String) -> Unit, + viewModel: SettingsViewModel = koinViewModel() ) { val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle() when (settingsUiState) { @@ -104,7 +105,7 @@ internal fun SettingsDialogContent( }, text = { Column { - Divider() + HorizontalDivider() BooleanSettingsContent( appState, editableSettings, @@ -112,7 +113,7 @@ internal fun SettingsDialogContent( ) SectionDivider() LinksPanelContent(openExternalUrl, openOssLicencesInfo) - Divider() + HorizontalDivider() } }, confirmButton = { diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog_ui_parts.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog_ui_parts.kt index e13b041..63d89c6 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog_ui_parts.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/settings/dialog_ui_parts.kt @@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -46,7 +46,7 @@ internal fun LinksPanelContent( FlowRow( horizontalArrangement = Arrangement.spacedBy( space = 16.dp, - alignment = Alignment.Start, + alignment = Alignment.CenterHorizontally, ), modifier = Modifier.fillMaxWidth(), ) { @@ -82,7 +82,7 @@ internal fun LinksPanelContent( */ @Composable fun SectionDivider() { - Divider(Modifier.padding(top = 8.dp)) + HorizontalDivider(Modifier.padding(top = 8.dp)) } /** @@ -100,7 +100,7 @@ internal fun BooleanSettingsContent( editableSettings: UserEditableSettings, onBooleanSettingUpdated: (String, Boolean) -> Unit ) { - if (appState.is7InTabletWidth) { + if (appState.isMediumWidth) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/WelcomeRoute.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/WelcomeRoute.kt new file mode 100644 index 0000000..f786292 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/WelcomeRoute.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.features.welcome.screens.BookWelcomeScreen +import dev.marlonlom.apps.cappajv.features.welcome.screens.LandscapeCompactWelcomeScreen +import dev.marlonlom.apps.cappajv.features.welcome.screens.PortraitWelcomeScreen +import dev.marlonlom.apps.cappajv.features.welcome.screens.TableTopWelcomeScreen +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState +import dev.marlonlom.apps.cappajv.ui.main.scaffold.ScaffoldContentType + +/** + * Welcome route composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param onContinueHomeButtonClicked Action for continue button clicked. + */ +@Composable +fun WelcomeRoute( + appState: CappajvAppState, + onContinueHomeButtonClicked: () -> Unit +) { + val contentHorizontalPadding = when { + appState.devicePosture is DevicePosture.Separating.Book -> 0.dp + appState.isCompactWidth.not() -> 80.dp + else -> 20.dp + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = contentHorizontalPadding), + contentAlignment = Alignment.Center + ) { + when (appState.scaffoldContentType) { + ScaffoldContentType.SinglePane -> { + when { + appState.isCompactHeight -> { + LandscapeCompactWelcomeScreen(appState, onContinueHomeButtonClicked) + } + + else -> { + PortraitWelcomeScreen(appState, onContinueHomeButtonClicked) + } + } + } + + is ScaffoldContentType.TwoPane -> { + when (appState.devicePosture) { + is DevicePosture.Separating.TableTop -> { + TableTopWelcomeScreen(appState, onContinueHomeButtonClicked) + } + + is DevicePosture.Separating.Book -> { + BookWelcomeScreen(appState, onContinueHomeButtonClicked) + } + + else -> { + PortraitWelcomeScreen(appState, onContinueHomeButtonClicked) + } + } + + } + } + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeButton.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeButton.kt new file mode 100644 index 0000000..98afbba --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeButton.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.parts + +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import dev.marlonlom.apps.cappajv.R + +@Composable +internal fun WelcomeButton( + onContinueHomeButtonClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + modifier = modifier, + onClick = { + onContinueHomeButtonClicked() + }, + shape = MaterialTheme.shapes.small, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) + ) { + Text( + text = stringResource(R.string.text_welcome_button), + style = MaterialTheme.typography.bodyLarge, + ) + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeDetail.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeDetail.kt new file mode 100644 index 0000000..aa13238 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeDetail.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.parts + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.R +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState + +/** + * Details text for welcome screen composable ui. + * + * @author marlonlom + * + * @param appState + */ +@Composable +internal fun WelcomeDetail(appState: CappajvAppState) { + Text( + modifier = Modifier + .padding(horizontal = 20.dp), + text = stringResource(R.string.text_welcome_detail), + style = MaterialTheme.typography.bodyLarge, + textAlign = getWelcomeTextAlign(appState), + fontWeight = FontWeight.Normal + ) +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomePosterImage.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomePosterImage.kt new file mode 100644 index 0000000..47b3b4a --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomePosterImage.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.parts + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.R + +/** + * Poster image for welcome screen composable ui. + * + * @author marlonlom + * + */ +@Composable +internal fun WelcomePosterImage() { + Image( + painter = painterResource(id = R.drawable.img_welcome_poster), + contentDescription = null, + modifier = Modifier + .height(200.dp), + contentScale = ContentScale.Crop + ) +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeTitle.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeTitle.kt new file mode 100644 index 0000000..09c5989 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/parts/WelcomeTitle.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.parts + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.R +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState + + +/** + * Title text for welcome screen composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + */ +@Composable +internal fun WelcomeTitle(appState: CappajvAppState) { + Text( + modifier = Modifier + .paddingFromBaseline(top = 40.dp, bottom = 20.dp) + .padding(horizontal = 20.dp), + text = stringResource(R.string.text_welcome_title), + style = MaterialTheme.typography.titleLarge, + textAlign = getWelcomeTextAlign(appState), + fontWeight = FontWeight.Bold + ) +} + +@Composable +internal fun getWelcomeTextAlign(appState: CappajvAppState): TextAlign = when { + appState.isCompactHeight -> TextAlign.Start + else -> TextAlign.Center +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens.kt deleted file mode 100644 index e2c5d6f..0000000 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens.kt +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright 2024 Marlonlom - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.marlonlom.apps.cappajv.features.welcome - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState - -/** - * Welcome route composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -fun WelcomeRoute( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit -) { - val contentHorizontalPadding = when { - appState.isDeviceBookPosture -> 0.dp - appState.isCompactWidth.not() -> 80.dp - else -> 20.dp - } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = contentHorizontalPadding), - contentAlignment = Alignment.Center - ) { - when { - appState.isDeviceBookPostureVertical.and(appState.is10InTabletWidth.not()) -> { - FoldedPortraitWelcomeScreen(appState, onContinueHomeButtonClicked) - } - - appState.isDeviceBookPostureHorizontal.and(appState.is10InTabletWidth.not()) -> { - FoldedLandscapeWelcomeScreen(appState, onContinueHomeButtonClicked) - } - - appState.isDeviceSeparatingHorizontal.and(appState.is10InTabletWidth.not()) -> { - FoldingLandscapeWelcomeScreen(appState, onContinueHomeButtonClicked) - } - - appState.isDeviceSeparatingVertical.and(appState.is10InTabletWidth.not()) -> { - FoldingPortraitWelcomeScreen(appState, onContinueHomeButtonClicked) - } - - appState.isCompactHeight -> { - LandscapeCompactWelcomeScreen(appState, onContinueHomeButtonClicked) - } - - else -> { - PortraitWelcomeScreen(appState, onContinueHomeButtonClicked) - } - } - } -} - -/** - * Folded Landscape welcome screen composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -fun FoldedLandscapeWelcomeScreen( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit -) { - Column(modifier = Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.5f) - .padding(horizontal = 20.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier - .fillMaxWidth(0.5f) - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - WelcomePosterImage() - } - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - WelcomeTitle(appState) - WelcomeDetail(appState) - WelcomeButton(onContinueHomeButtonClicked) - } - } - Row( - modifier = Modifier - .fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - - } - } -} - -/** - * Folded Portrait welcome screen composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -internal fun FoldedPortraitWelcomeScreen( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit, -) { - Row(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxWidth(0.5f) - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - WelcomePosterImage() - WelcomeTitle(appState) - WelcomeDetail(appState) - WelcomeButton(onContinueHomeButtonClicked) - } - Column( - modifier = Modifier - .padding(top = 40.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - - } - } -} - -/** - * Folding Portrait welcome screen composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -internal fun FoldingPortraitWelcomeScreen( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit, -) { - Row(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxWidth(0.5f) - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - WelcomePosterImage() - } - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 40.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - WelcomeTitle(appState) - WelcomeDetail(appState) - WelcomeButton(onContinueHomeButtonClicked) - } - } -} - -/** - * Folding Landscape welcome screen composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -internal fun FoldingLandscapeWelcomeScreen( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit, -) { - Column(modifier = Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.5f), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - WelcomePosterImage() - } - Row( - modifier = Modifier - .fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 40.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - WelcomeTitle(appState) - WelcomeDetail(appState) - WelcomeButton(onContinueHomeButtonClicked) - } - } - } -} - -/** - * Landscape compact welcome screen composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -internal fun LandscapeCompactWelcomeScreen( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit, -) { - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(0.75f), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.fillMaxWidth(0.35f)) { - WelcomePosterImage() - } - Column(modifier = Modifier.fillMaxWidth()) { - WelcomeTitle(appState) - WelcomeDetail(appState) - } - } - Row( - modifier = Modifier.fillMaxWidth(0.6f), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - WelcomeButton(onContinueHomeButtonClicked) - } - } -} - -/** - * Portrait welcome screen composable ui. - * - * @author marlonlom - * - * @param appState Application ui state. - * @param onContinueHomeButtonClicked Action for continue button clicked. - */ -@Composable -internal fun PortraitWelcomeScreen( - appState: CappajvAppState, - onContinueHomeButtonClicked: () -> Unit, -) { - val contentModifier = when { - appState.is10InTabletWidth -> Modifier.fillMaxWidth(0.5f) - appState.is7InTabletWidth -> Modifier.fillMaxWidth(0.75f) - else -> Modifier.fillMaxWidth() - } - - Column( - modifier = contentModifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - WelcomePosterImage() - WelcomeTitle(appState) - WelcomeDetail(appState) - WelcomeButton(onContinueHomeButtonClicked) - } -} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/BookWelcomeScreen.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/BookWelcomeScreen.kt new file mode 100644 index 0000000..12e23fe --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/BookWelcomeScreen.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeButton +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeDetail +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomePosterImage +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeTitle +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState + +/** + * Book mode folding welcome screen composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param onContinueHomeButtonClicked Action for continue button clicked. + */ +@Composable +internal fun BookWelcomeScreen( + appState: CappajvAppState, + onContinueHomeButtonClicked: () -> Unit, +) { + val columnWidthRatio = when (appState.devicePosture) { + is DevicePosture.Separating.Book -> appState.devicePosture.hingeRatio + else -> 0.5f + } + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.fillMaxWidth(columnWidthRatio), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + WelcomePosterImage() + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + WelcomeTitle(appState) + WelcomeDetail(appState) + WelcomeButton( + modifier = Modifier.padding(20.dp), + onContinueHomeButtonClicked = onContinueHomeButtonClicked + ) + } + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/LandscapeCompactWelcomeScreen.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/LandscapeCompactWelcomeScreen.kt new file mode 100644 index 0000000..3a1f914 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/LandscapeCompactWelcomeScreen.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeButton +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeDetail +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomePosterImage +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeTitle +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState + +/** + * Landscape compact welcome screen composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param onContinueHomeButtonClicked Action for continue button clicked. + */ +@Composable +internal fun LandscapeCompactWelcomeScreen( + appState: CappajvAppState, + onContinueHomeButtonClicked: () -> Unit, +) { + val contentMaxWidthRatio = if (appState.isMediumWidth) 1f else 0.75f + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(contentMaxWidthRatio), + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + horizontalAlignment = Alignment.End, + ) { + WelcomePosterImage() + } + Column(modifier = Modifier.fillMaxWidth()) { + WelcomeTitle(appState) + WelcomeDetail(appState) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + WelcomeButton( + modifier = Modifier.padding(20.dp), + onContinueHomeButtonClicked = onContinueHomeButtonClicked + ) + } + } + } + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/PortraitWelcomeScreen.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/PortraitWelcomeScreen.kt new file mode 100644 index 0000000..4084185 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/PortraitWelcomeScreen.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeButton +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeDetail +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomePosterImage +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeTitle +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState + +/** + * Portrait welcome screen composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param onContinueHomeButtonClicked Action for continue button clicked. + */ +@Composable +internal fun PortraitWelcomeScreen( + appState: CappajvAppState, + onContinueHomeButtonClicked: () -> Unit, +) { + val contentModifier = when { + appState.isExpandedWidth -> Modifier.fillMaxWidth(0.5f) + appState.isMediumWidth -> Modifier.fillMaxWidth(0.75f) + else -> Modifier.fillMaxWidth() + } + + Column( + modifier = contentModifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + WelcomePosterImage() + WelcomeTitle(appState) + WelcomeDetail(appState) + WelcomeButton( + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(bottom = 20.dp, top = 40.dp), + onContinueHomeButtonClicked = onContinueHomeButtonClicked + ) + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/TableTopWelcomeScreen.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/TableTopWelcomeScreen.kt new file mode 100644 index 0000000..e173d04 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/screens/TableTopWelcomeScreen.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.features.welcome.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeButton +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeDetail +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomePosterImage +import dev.marlonlom.apps.cappajv.features.welcome.parts.WelcomeTitle +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState + +/** + * TableTop mode folding welcome screen composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param onContinueHomeButtonClicked Action for continue button clicked. + */ +@Composable +internal fun TableTopWelcomeScreen( + appState: CappajvAppState, + onContinueHomeButtonClicked: () -> Unit, +) { + val columnHeightRatio = when (appState.devicePosture) { + is DevicePosture.Separating.TableTop -> appState.devicePosture.hingeRatio + else -> 0.5f + } + + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(columnHeightRatio), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + WelcomePosterImage() + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + WelcomeTitle(appState) + WelcomeDetail(appState) + WelcomeButton( + modifier = Modifier.padding(20.dp), + onContinueHomeButtonClicked = onContinueHomeButtonClicked + ) + } + } + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/ui_parts.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/ui_parts.kt deleted file mode 100644 index 7753eb0..0000000 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/welcome/ui_parts.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2024 Marlonlom - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.marlonlom.apps.cappajv.features.welcome - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.paddingFromBaseline -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import dev.marlonlom.apps.cappajv.R -import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState - -@Composable -internal fun WelcomePosterImage() { - Image( - painter = painterResource(id = R.drawable.img_welcome_poster), - contentDescription = null, - modifier = Modifier - .height(160.dp), - contentScale = ContentScale.Crop - ) -} - -@Composable -internal fun WelcomeButton(onContinueHomeButtonClicked: () -> Unit) { - Button( - modifier = Modifier.padding(top = 40.dp, bottom = 20.dp), - onClick = { - onContinueHomeButtonClicked() - }, - shape = MaterialTheme.shapes.small, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ) - ) { - Text(text = stringResource(R.string.text_welcome_button)) - } -} - -@Composable -internal fun WelcomeDetail(appState: CappajvAppState) { - Text( - modifier = Modifier - .padding(horizontal = 20.dp), - text = stringResource(R.string.text_welcome_detail), - style = MaterialTheme.typography.bodyMedium, - textAlign = getWelcomeTextAlign(appState), - fontWeight = FontWeight.Normal - ) -} - -@Composable -internal fun WelcomeTitle(appState: CappajvAppState) { - Text( - modifier = Modifier - .paddingFromBaseline(top = 40.dp, bottom = 20.dp) - .padding(horizontal = 20.dp), - text = stringResource(R.string.text_welcome_title), - style = MaterialTheme.typography.titleLarge, - textAlign = getWelcomeTextAlign(appState), - fontWeight = FontWeight.Bold - ) -} - -@Composable -private fun getWelcomeTextAlign(appState: CappajvAppState): TextAlign = when { - appState.isLandscapeOrientation.and(appState.isCompactHeight) -> TextAlign.Start - else -> TextAlign.Center -} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/layout/DevicePosture.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/layout/DevicePosture.kt new file mode 100644 index 0000000..6266f84 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/layout/DevicePosture.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.ui.layout + + +import android.graphics.Rect +import androidx.window.layout.FoldingFeature + +/** + * Information about the posture of the device. + * + * @author marlonlom + */ +sealed interface DevicePosture { + + /** + * Normal posture of the device. + * + * @author marlonlom + */ + data object Normal : DevicePosture + + /** + * Separating posture of the half opened device. + * + * @author marlonlom + * + */ + sealed interface Separating : DevicePosture { + /** Folding feature rectangle bounds. */ + val bounds: Rect + + /** Hinge ratio for screen separation. */ + val hingeRatio: Float + + /** Folding feature axis orientation indication. */ + val orientation: FoldingFeature.Orientation + + /** Indicates if folding feature is separating. */ + val isSeparating: Boolean + + /** + * Book posture of the device. + * + * @author marlonlom + * + * @property bounds Folding feature rectangle bounds. + * @property hingeRatio Hinge ratio for screen separation. + * @property orientation Folding feature axis orientation indication. + * @property isSeparating True/False indicating if folding feature is separating. + */ + data class Book( + override val bounds: Rect, + override val hingeRatio: Float, + override val orientation: FoldingFeature.Orientation, + override val isSeparating: Boolean, + ) : Separating + + /** + * Table top posture of the device. + * + * @author marlonlom + * + * @property bounds Folding feature rectangle bounds. + * @property hingeRatio Hinge ratio for screen separation. + * @property orientation Folding feature axis orientation indication. + * @property isSeparating True/False indicating if folding feature is separating. + */ + data class TableTop( + override val bounds: Rect, + override val hingeRatio: Float, + override val orientation: FoldingFeature.Orientation, + override val isSeparating: Boolean, + ) : Separating + + } + +} + diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/layout/DevicePostureDetector.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/layout/DevicePostureDetector.kt new file mode 100644 index 0000000..6e0201b --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/layout/DevicePostureDetector.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.ui.layout + +import androidx.window.layout.FoldingFeature + +/** + * Device posture detector single object that uses [FoldingFeature] information. + * + * @author marlonlom + */ +object DevicePostureDetector { + + /** + * Returns the device posture for selected layout information. + * + * @param foldingFeature Folding feature from application window. + */ + @JvmStatic + fun fromLayoutInfo(foldingFeature: FoldingFeature?): DevicePosture = when { + + foldingFeature != null && isBookMode(foldingFeature) -> { + DevicePosture.Separating.Book( + bounds = foldingFeature.bounds, + hingeRatio = getHingeRatio(foldingFeature), + orientation = foldingFeature.orientation, + isSeparating = foldingFeature.isSeparating, + ) + } + + foldingFeature != null && isTableTopMode(foldingFeature) -> { + DevicePosture.Separating.TableTop( + bounds = foldingFeature.bounds, + hingeRatio = getHingeRatio(foldingFeature), + orientation = foldingFeature.orientation, + isSeparating = foldingFeature.isSeparating, + ) + } + + else -> DevicePosture.Normal + } + + private fun getHingeRatio( + foldFeature: FoldingFeature + ): Float = when (foldFeature.orientation) { + FoldingFeature.Orientation.VERTICAL -> { + val screenWidth = foldFeature.bounds.left + foldFeature.bounds.right + foldFeature.bounds.left.toFloat() / screenWidth.toFloat() + } + + else -> { + val screenHeight = foldFeature.bounds.top + foldFeature.bounds.bottom + foldFeature.bounds.top.toFloat() / screenHeight.toFloat() + } + } + + + private fun isBookMode(foldingFeature: FoldingFeature) = foldingFeature.let { + return@let when (it.state) { + FoldingFeature.State.HALF_OPENED -> { + it.state == FoldingFeature.State.HALF_OPENED + && it.orientation == FoldingFeature.Orientation.VERTICAL + && it.occlusionType == FoldingFeature.OcclusionType.NONE + } + + FoldingFeature.State.FLAT -> { + it.state == FoldingFeature.State.FLAT + && it.orientation == FoldingFeature.Orientation.VERTICAL + && it.occlusionType == FoldingFeature.OcclusionType.FULL + } + + else -> false + } + } + + private fun isTableTopMode(foldingFeature: FoldingFeature) = foldingFeature.let { + return@let when (it.state) { + FoldingFeature.State.HALF_OPENED -> { + it.state == FoldingFeature.State.HALF_OPENED + && it.orientation == FoldingFeature.Orientation.HORIZONTAL + && it.occlusionType == FoldingFeature.OcclusionType.NONE + } + + FoldingFeature.State.FLAT -> { + it.state == FoldingFeature.State.FLAT + && it.orientation == FoldingFeature.Orientation.HORIZONTAL + && it.occlusionType == FoldingFeature.OcclusionType.FULL + } + + else -> false + } + } + +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContent.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContent.kt index b58d22c..245e788 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContent.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContent.kt @@ -9,17 +9,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dev.marlonlom.apps.cappajv.core.preferences.UserPreferencesRepository -import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailViewModel -import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListViewModel +import dev.marlonlom.apps.cappajv.features.welcome.WelcomeRoute import dev.marlonlom.apps.cappajv.ui.main.scaffold.MainScaffold import dev.marlonlom.apps.cappajv.ui.theme.CappajvTheme -import dev.marlonlom.apps.cappajv.ui.util.DevicePosture import kotlinx.coroutines.ExperimentalCoroutinesApi /** @@ -59,10 +52,9 @@ private fun shouldUseDarkTheme( * @author marlonlom * * @param mainActivityUiState Main activity ui state. - * @param windowSizeClass Window size class. - * @param userPreferencesRepository User preferences repository. - * @param catalogListViewModel Catalog list viewmodel. - * @param catalogDetailViewModel Catalog detail viewmodel. + * @param appUiState Application ui state. + * @param appContentCallbacks Application content callbacks. + * @param onOnboardingComplete Action for onboarding complete. */ @ExperimentalFoundationApi @ExperimentalLayoutApi @@ -71,31 +63,29 @@ private fun shouldUseDarkTheme( @Composable fun AppContent( mainActivityUiState: MainActivityUiState, - windowSizeClass: WindowSizeClass, - devicePosture: DevicePosture, - userPreferencesRepository: UserPreferencesRepository, - catalogListViewModel: CatalogListViewModel, - catalogDetailViewModel: CatalogDetailViewModel, + appUiState: CappajvAppState, + appContentCallbacks: AppContentCallbacks, onOnboardingComplete: () -> Unit, ) = CappajvTheme( darkTheme = shouldUseDarkTheme(mainActivityUiState), dynamicColor = shouldUseDynamicColor(mainActivityUiState) ) { + when (mainActivityUiState) { + MainActivityUiState.Loading -> Unit - val catalogListState by catalogListViewModel.uiState.collectAsStateWithLifecycle() - - val appContentCallbacks = newAppContentCallbacks( - activityContext = LocalContext.current, - catalogDetailViewModel = catalogDetailViewModel - ) - - MainScaffold( - mainActivityUiState = mainActivityUiState, - windowSizeClass = windowSizeClass, - devicePosture = devicePosture, - appContentCallbacks = appContentCallbacks, - userPreferencesRepository = userPreferencesRepository, - onOnboardingComplete = onOnboardingComplete, - catalogListState = catalogListState - ) + is MainActivityUiState.Success -> { + if (mainActivityUiState.userData.isOnboarding) { + WelcomeRoute( + appState = appUiState, + onContinueHomeButtonClicked = onOnboardingComplete + ) + } else { + MainScaffold( + mainActivityUiState = mainActivityUiState, + appState = appUiState, + appContentCallbacks = appContentCallbacks, + ) + } + } + } } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContentCallbacks.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContentCallbacks.kt index ace214a..6a6d66c 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContentCallbacks.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppContentCallbacks.kt @@ -10,7 +10,6 @@ import android.content.Intent import androidx.compose.runtime.Composable import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetail -import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailViewModel import dev.marlonlom.apps.cappajv.ui.util.CustomTabsOpener import kotlinx.coroutines.ExperimentalCoroutinesApi import timber.log.Timber @@ -41,13 +40,11 @@ data class AppContentCallbacks( * @author marlonlom * * @param activityContext Activity context. - * @param catalogDetailViewModel Catalog details viewmodel. */ @ExperimentalCoroutinesApi @Composable internal fun newAppContentCallbacks( activityContext: Context, - catalogDetailViewModel: CatalogDetailViewModel ) = AppContentCallbacks( onOnboardingCompleter = { diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppState.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppState.kt index e487d6c..7db4534 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppState.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/AppState.kt @@ -5,21 +5,18 @@ package dev.marlonlom.apps.cappajv.ui.main -import android.content.res.Configuration import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.dp import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import androidx.window.layout.FoldingFeature -import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListState -import dev.marlonlom.apps.cappajv.ui.util.DevicePosture +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture +import dev.marlonlom.apps.cappajv.ui.main.scaffold.ScaffoldContentClassifier +import dev.marlonlom.apps.cappajv.ui.navigation.NavigationTypeSelector /** * Remembers the application ui state value. @@ -28,7 +25,6 @@ import dev.marlonlom.apps.cappajv.ui.util.DevicePosture * * @param windowSizeClass Window size class. * @param navController Navigation controller. - * @param localConfiguration Local configuration object. * * @return Application ui state value. */ @@ -36,22 +32,19 @@ import dev.marlonlom.apps.cappajv.ui.util.DevicePosture fun rememberCappajvAppState( windowSizeClass: WindowSizeClass, devicePosture: DevicePosture, + screenWidthDp: Int, navController: NavHostController = rememberNavController(), - localConfiguration: Configuration = LocalConfiguration.current, - catalogListState: CatalogListState, ): CappajvAppState = remember( windowSizeClass, devicePosture, + screenWidthDp, navController, - localConfiguration, - catalogListState ) { CappajvAppState( navController = navController, windowSizeClass = windowSizeClass, - localConfiguration = localConfiguration, devicePosture = devicePosture, - catalogListState = catalogListState + screenWidthDp = screenWidthDp, ) } @@ -62,73 +55,31 @@ fun rememberCappajvAppState( * * @param navController Navigation controller. * @param windowSizeClass Window size class. - * @param localConfiguration Local configuration. * @property devicePosture Device posture, used for detecting foldable features. - * @property catalogListState Catalog list ui state value. */ @Stable data class CappajvAppState( internal val navController: NavHostController, val windowSizeClass: WindowSizeClass, - private val localConfiguration: Configuration, val devicePosture: DevicePosture, - val catalogListState: CatalogListState, + val screenWidthDp: Int, ) { val isCompactWidth get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact val isCompactHeight get() = windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact - val isLandscapeOrientation get() = localConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE + val isMediumWidth get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Medium - val is7InTabletWidth get() = localConfiguration.smallestScreenWidthDp.dp >= 600.dp + val isExpandedWidth get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded - val is10InTabletWidth get() = localConfiguration.smallestScreenWidthDp.dp >= 720.dp + val isLandscape get() = isMediumWidth.and(isCompactHeight).or(isExpandedWidth.and(isCompactHeight.not())) - val canShowBottomNavigation get() = isCompactWidth + val scaffoldContentType + get() = ScaffoldContentClassifier.classify( + devicePosture, isExpandedWidth, isMediumWidth, isCompactHeight + ) - val canShowNavigationRail get() = isCompactWidth.not().and(is10InTabletWidth.not()) - - val canShowExpandedNavigationDrawer get() = isCompactWidth.not().and(is10InTabletWidth) - - val isDeviceBookPosture get() = devicePosture is DevicePosture.BookPosture - - val isDeviceBookPostureVertical - get() = when (devicePosture) { - is DevicePosture.BookPosture -> { - devicePosture.orientation == FoldingFeature.Orientation.VERTICAL - } - - else -> false - } - - val isDeviceBookPostureHorizontal - get() = when (devicePosture) { - is DevicePosture.BookPosture -> { - devicePosture.orientation == FoldingFeature.Orientation.HORIZONTAL - } - - else -> false - } - - val isDeviceSeparating get() = devicePosture is DevicePosture.Separating - - val isDeviceSeparatingVertical - get() = when (devicePosture) { - is DevicePosture.Separating -> { - devicePosture.orientation == FoldingFeature.Orientation.VERTICAL - } - - else -> false - } - - val isDeviceSeparatingHorizontal - get() = when (devicePosture) { - is DevicePosture.Separating -> { - devicePosture.orientation == FoldingFeature.Orientation.HORIZONTAL - } - - else -> false - } + val navigationType get() = NavigationTypeSelector.fromWindowSize(windowSizeClass, devicePosture, screenWidthDp) /** * Changes selected top destination. diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/MainActivity.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/MainActivity.kt index 0250f1c..1e1402d 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/MainActivity.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/MainActivity.kt @@ -9,7 +9,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.material3.ExperimentalMaterial3Api @@ -18,24 +17,17 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalConfiguration import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker -import dev.marlonlom.apps.cappajv.core.catalog_source.CatalogDataService -import dev.marlonlom.apps.cappajv.core.database.CappaDatabase -import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSource -import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSourceImpl -import dev.marlonlom.apps.cappajv.core.preferences.UserPreferencesRepository -import dev.marlonlom.apps.cappajv.dataStore -import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailRepository -import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailViewModel -import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListRepository -import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListViewModel -import dev.marlonlom.apps.cappajv.ui.util.DevicePosture +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture +import dev.marlonlom.apps.cappajv.ui.layout.DevicePostureDetector import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collect @@ -43,12 +35,15 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.viewModel +import kotlin.contracts.ExperimentalContracts /** * Main activity class. * * @author marlonlom */ +@ExperimentalContracts @ExperimentalFoundationApi @ExperimentalLayoutApi @ExperimentalCoroutinesApi @@ -56,27 +51,7 @@ import kotlinx.coroutines.launch @ExperimentalMaterial3WindowSizeClassApi class MainActivity : ComponentActivity() { - private val newUserPreferencesRepository: UserPreferencesRepository get() = UserPreferencesRepository(dataStore) - - private val mainActivityViewModel: MainActivityViewModel by viewModels( - factoryProducer = { - MainActivityViewModel.factory(newUserPreferencesRepository) - }) - - private val catalogListViewModel: CatalogListViewModel by viewModels( - factoryProducer = { - CatalogListViewModel.factory( - CatalogListRepository( - localDataSource = newLocalDataSource(), - catalogDataService = CatalogDataService() - ) - ) - }) - - private val catalogDetailViewModel: CatalogDetailViewModel by viewModels( - factoryProducer = { - CatalogDetailViewModel.factory(CatalogDetailRepository(newLocalDataSource())) - }) + private val mainActivityViewModel: MainActivityViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,9 +62,7 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - mainActivityViewModel.uiState - .onEach { mainActivityUiState = it } - .collect() + mainActivityViewModel.uiState.onEach { mainActivityUiState = it }.collect() } } @@ -102,41 +75,36 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() + val devicePostureFlow = WindowInfoTracker + .getOrCreate(this@MainActivity) + .windowLayoutInfo(this@MainActivity) + .flowWithLifecycle(lifecycle) + .map { layoutInfo -> + val foldingFeature = layoutInfo.displayFeatures + .filterIsInstance(FoldingFeature::class.java) + .firstOrNull() + DevicePostureDetector.fromLayoutInfo(foldingFeature) + }.stateIn( + scope = lifecycleScope, + started = SharingStarted.Eagerly, + initialValue = DevicePosture.Normal + ) + setContent { - val windowSizeClass = calculateWindowSizeClass(this) val devicePosture by devicePostureFlow.collectAsStateWithLifecycle() + val appState = rememberCappajvAppState( + windowSizeClass = calculateWindowSizeClass(this), + devicePosture = devicePosture, + screenWidthDp = LocalConfiguration.current.smallestScreenWidthDp + ) AppContent( mainActivityUiState = mainActivityUiState, - windowSizeClass = windowSizeClass, - devicePosture = devicePosture, - userPreferencesRepository = newUserPreferencesRepository, - catalogListViewModel = catalogListViewModel, - catalogDetailViewModel = catalogDetailViewModel, - onOnboardingComplete = { - mainActivityViewModel.setOnboardingComplete() - }, + appUiState = appState, + appContentCallbacks = newAppContentCallbacks(applicationContext), + onOnboardingComplete = mainActivityViewModel::setOnboardingComplete, ) } } - private fun newLocalDataSource(): LocalDataSource { - val cappaDatabase = CappaDatabase.getInstance(applicationContext) - return LocalDataSourceImpl( - catalogItemsDao = cappaDatabase.catalogProductsDao(), - catalogFavoriteItemsDao = cappaDatabase.catalogFavoriteItemsDao(), - catalogPunctuationsDao = cappaDatabase.catalogPunctuationsDao(), - ) - } - - private val devicePostureFlow = WindowInfoTracker - .getOrCreate(this@MainActivity) - .windowLayoutInfo(this@MainActivity) - .flowWithLifecycle(lifecycle) - .map { layoutInfo -> DevicePosture.fromLayoutInfo(layoutInfo) } - .stateIn( - scope = lifecycleScope, - started = SharingStarted.Eagerly, - initialValue = DevicePosture.NormalPosture - ) } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffold.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffold.kt index 9f54012..12335a2 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffold.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffold.kt @@ -6,37 +6,23 @@ package dev.marlonlom.apps.cappajv.ui.main.scaffold import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.navigation.compose.currentBackStackEntryAsState -import dev.marlonlom.apps.cappajv.core.preferences.UserPreferencesRepository -import dev.marlonlom.apps.cappajv.features.catalog_list.CatalogListState import dev.marlonlom.apps.cappajv.features.settings.SettingsDialog -import dev.marlonlom.apps.cappajv.features.settings.SettingsViewModel -import dev.marlonlom.apps.cappajv.features.welcome.WelcomeRoute import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState import dev.marlonlom.apps.cappajv.ui.main.MainActivityUiState -import dev.marlonlom.apps.cappajv.ui.main.rememberCappajvAppState import dev.marlonlom.apps.cappajv.ui.navigation.CatalogDestination -import dev.marlonlom.apps.cappajv.ui.navigation.MainNavHost -import dev.marlonlom.apps.cappajv.ui.util.DevicePosture +import dev.marlonlom.apps.cappajv.ui.navigation.NavigationType /** * Main scaffold composable ui. @@ -44,11 +30,8 @@ import dev.marlonlom.apps.cappajv.ui.util.DevicePosture * @author marlonlom * * @param mainActivityUiState Main activity ui state. - * @param windowSizeClass Window size class. - * @param appContentCallbacks Application content callbacks. - * @param userPreferencesRepository User preferences repository. - * @param onOnboardingComplete Action for onboarding complete event. * @param appState Main application ui state + * @param appContentCallbacks Application content callbacks. */ @ExperimentalFoundationApi @ExperimentalMaterial3Api @@ -56,21 +39,12 @@ import dev.marlonlom.apps.cappajv.ui.util.DevicePosture @Composable fun MainScaffold( mainActivityUiState: MainActivityUiState, - windowSizeClass: WindowSizeClass, - devicePosture: DevicePosture, + appState: CappajvAppState, appContentCallbacks: AppContentCallbacks, - userPreferencesRepository: UserPreferencesRepository, - onOnboardingComplete: () -> Unit, - catalogListState: CatalogListState, - appState: CappajvAppState = rememberCappajvAppState( - windowSizeClass = windowSizeClass, - devicePosture = devicePosture, - catalogListState = catalogListState - ), ) { - val currentAppRoute = appState.navController - .currentBackStackEntryAsState().value?.destination?.route ?: CatalogDestination.CatalogList.route + val currentAppRoute = appState.navController.currentBackStackEntryAsState().value?.destination?.route + ?: CatalogDestination.CatalogList.route var bottomNavSelectedIndex by rememberSaveable { mutableIntStateOf( CatalogDestination.topCatalogDestinations.map { it.route }.indexOf(currentAppRoute) @@ -83,7 +57,6 @@ fun MainScaffold( if (showSettingsDialog) { SettingsDialog( appState = appState, - viewModel = SettingsViewModel(repository = userPreferencesRepository), onDialogDismissed = { showSettingsDialog = false }, openOssLicencesInfo = appContentCallbacks.openOssLicencesInfo, openExternalUrl = appContentCallbacks.openExternalUrl @@ -95,14 +68,13 @@ fun MainScaffold( bottomBar = { val isCurrentlyOnboarding: (MainActivityUiState) -> Boolean = { uiState -> when (uiState) { - is MainActivityUiState.Success -> - uiState.userData.isOnboarding + is MainActivityUiState.Success -> uiState.userData.isOnboarding else -> true } } - val isBottomBarVisible = appState.canShowBottomNavigation.and(isTopDestination) + val isBottomBarVisible = (appState.navigationType == NavigationType.BOTTOM_NAV).and(isTopDestination) .and(isCurrentlyOnboarding(mainActivityUiState).not()) if (isBottomBarVisible) { @@ -122,91 +94,16 @@ fun MainScaffold( ) { paddingValues -> MainScaffoldContent( paddingValues = paddingValues, - mainActivityUiState = mainActivityUiState, appState = appState, appContentCallbacks = appContentCallbacks, - onOnboardingComplete = onOnboardingComplete, selectedPosition = bottomNavSelectedIndex, - onSelectedPositionChanged = { position, route -> - if (route == CatalogDestination.Settings.route) { - showSettingsDialog = true - } else { - bottomNavSelectedIndex = position - appState.changeTopDestination(route) - } - }, - ) - } -} - -/** - * Main scaffold content composable ui. - * - * @author marlonlom - * - * @param paddingValues Padding values. - * @param mainActivityUiState Main activity ui state. - * @param appState Application ui state. - * @param appContentCallbacks Application content callbacks. - * @param onOnboardingComplete Action for onboarding complete event. - */ -@ExperimentalFoundationApi -@ExperimentalMaterial3Api -@ExperimentalLayoutApi -@Composable -private fun MainScaffoldContent( - paddingValues: PaddingValues, - mainActivityUiState: MainActivityUiState, - appState: CappajvAppState, - appContentCallbacks: AppContentCallbacks, - onOnboardingComplete: () -> Unit, - selectedPosition: Int, - onSelectedPositionChanged: (Int, String) -> Unit -) { - Box( - modifier = Modifier - .safeDrawingPadding() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - when (mainActivityUiState) { - is MainActivityUiState.Success -> { - if (mainActivityUiState.userData.isOnboarding) { - WelcomeRoute( - appState = appState, - onContinueHomeButtonClicked = onOnboardingComplete - ) - } else { - if (appState.canShowExpandedNavigationDrawer) { - ExpandedNavigationDrawer( - selectedPosition = selectedPosition, - onSelectedPositionChanged = onSelectedPositionChanged, - ) { - MainNavHost( - appState = appState, - appContentCallbacks - ) - } - } else { - Row { - if (appState.canShowNavigationRail) { - MainNavigationRail( - selectedPosition = selectedPosition, - onSelectedPositionChanged = onSelectedPositionChanged, - ) - } - - MainNavHost( - appState = appState, - appContentCallbacks - ) - } - } - } + ) { position, route -> + if (route == CatalogDestination.Settings.route) { + showSettingsDialog = true + } else { + bottomNavSelectedIndex = position + appState.changeTopDestination(route) } - - else -> Unit } - } } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffoldContent.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffoldContent.kt new file mode 100644 index 0000000..47620a1 --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/MainScaffoldContent.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.ui.main.scaffold + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks +import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState +import dev.marlonlom.apps.cappajv.ui.navigation.MainNavHost +import dev.marlonlom.apps.cappajv.ui.navigation.NavigationType +import timber.log.Timber + + +/** + * Main scaffold content composable ui. + * + * @author marlonlom + * + * @param paddingValues Padding values. + * @param appState Application ui state. + * @param appContentCallbacks Application content callbacks. + * @param selectedPosition + * @param onSelectedPositionChanged + */ +@ExperimentalFoundationApi +@ExperimentalMaterial3Api +@ExperimentalLayoutApi +@Composable +internal fun MainScaffoldContent( + paddingValues: PaddingValues, + appState: CappajvAppState, + appContentCallbacks: AppContentCallbacks, + selectedPosition: Int, + onSelectedPositionChanged: (Int, String) -> Unit +) { + Timber.d( + """ + [MainScaffoldContent] + scaffoldContentType=${appState.scaffoldContentType} + devicePosture=${appState.devicePosture} + """.trimIndent() + ) + Box( + modifier = Modifier + .safeDrawingPadding() + .padding(paddingValues), + contentAlignment = Alignment.Center, + ) { + when (appState.navigationType) { + NavigationType.EXPANDED_NAV -> { + ExpandedNavigationDrawer( + selectedPosition = selectedPosition, + onSelectedPositionChanged = onSelectedPositionChanged, + ) { + MainNavHost( + appState = appState, appContentCallbacks + ) + } + } + + NavigationType.NAVIGATION_RAIL -> { + Row { + MainNavigationRail( + selectedPosition = selectedPosition, + onSelectedPositionChanged = onSelectedPositionChanged, + ) + + MainNavHost( + appState = appState, appContentCallbacks + ) + } + } + + NavigationType.BOTTOM_NAV -> { + MainNavHost( + appState = appState, appContentCallbacks + ) + } + } + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/ScaffoldContentType.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/ScaffoldContentType.kt new file mode 100644 index 0000000..3455f9d --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/main/scaffold/ScaffoldContentType.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.ui.main.scaffold + +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture +import dev.marlonlom.apps.cappajv.ui.main.scaffold.ScaffoldContentType.SinglePane +import dev.marlonlom.apps.cappajv.ui.main.scaffold.ScaffoldContentType.TwoPane + +/** + * Scaffold inner content type sealed interface definition. + * + * @author marlonlom + * + */ +sealed interface ScaffoldContentType { + + /** + * Single pane scaffold inner content type data object. + * + * @author marlonlom + * + */ + data object SinglePane : ScaffoldContentType + + /** + * Two pane scaffold inner content type data object. + * + * @author marlonlom + * + * @property hingeRatio Hinge ratio as percentage number. + */ + data class TwoPane( + val hingeRatio: Float = 0.5f + ) : ScaffoldContentType +} + +/** + * Scaffold inner content classifier single object. + * + * @author marlonlom + * + */ +object ScaffoldContentClassifier { + + /** + * Indicated scaffold inner content type by window size information and device posture. + * + * @param devicePosture + * + * @return Scaffold inner content type. + */ + @JvmStatic + fun classify( + devicePosture: DevicePosture, + isExpandedWidth: Boolean, + isMediumWidth: Boolean, + isCompactHeight: Boolean, + ): ScaffoldContentType = when { + + isMediumWidth.not().and(!isExpandedWidth.not()) -> when (devicePosture) { + is DevicePosture.Separating.TableTop -> TwoPane(devicePosture.hingeRatio) + else -> SinglePane + } + + isExpandedWidth.and(isCompactHeight.not()) -> when (devicePosture) { + DevicePosture.Normal -> TwoPane() + is DevicePosture.Separating.TableTop -> TwoPane(devicePosture.hingeRatio) + is DevicePosture.Separating.Book -> TwoPane(devicePosture.hingeRatio) + } + + isMediumWidth.and(isCompactHeight.not()) -> when (devicePosture) { + DevicePosture.Normal -> SinglePane + is DevicePosture.Separating.TableTop -> TwoPane(devicePosture.hingeRatio) + is DevicePosture.Separating.Book -> TwoPane(devicePosture.hingeRatio) + } + + isCompactHeight.and(devicePosture is DevicePosture.Normal) -> SinglePane + + else -> SinglePane + } +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/navigation/NavigationType.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/navigation/NavigationType.kt new file mode 100644 index 0000000..9e0a28b --- /dev/null +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/navigation/NavigationType.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Marlonlom + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.marlonlom.apps.cappajv.ui.navigation + +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import dev.marlonlom.apps.cappajv.ui.layout.DevicePosture + +object NavigationTypeSelector { + + @JvmStatic + fun fromWindowSize( + wsc: WindowSizeClass, + devicePosture: DevicePosture, + screenWidthDp: Int, + ): NavigationType = when { + screenWidthDp >= 720 -> NavigationType.EXPANDED_NAV + screenWidthDp >= 600 -> NavigationType.NAVIGATION_RAIL + isCompactHeight(devicePosture, wsc) -> NavigationType.NAVIGATION_RAIL + isFullyMediumWidth(devicePosture, wsc) -> NavigationType.NAVIGATION_RAIL + else -> NavigationType.BOTTOM_NAV + } + + private fun isCompactHeight(devicePosture: DevicePosture, wsc: WindowSizeClass) = + devicePosture == DevicePosture.Normal && wsc.heightSizeClass == WindowHeightSizeClass.Compact + + private fun isFullyMediumWidth(devicePosture: DevicePosture, wsc: WindowSizeClass) = + devicePosture == DevicePosture.Normal && wsc.widthSizeClass == WindowWidthSizeClass.Medium +} + +enum class NavigationType { + BOTTOM_NAV, NAVIGATION_RAIL, EXPANDED_NAV +} diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/util/DevicePostureUtil.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/util/DevicePostureUtil.kt deleted file mode 100644 index 3cc0746..0000000 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/ui/util/DevicePostureUtil.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2024 Marlonlom - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.marlonlom.apps.cappajv.ui.util - -import android.graphics.Rect -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowLayoutInfo -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -/** - * Information about the posture of the device. - * - * @author marlonlom - */ -sealed interface DevicePosture { - - /** - * Normal posture posture of the device. - * - * @author marlonlom - */ - data object NormalPosture : DevicePosture - - /** - * Book posture posture of the foldable device. - * - * @author marlonlom - * - * @property hingePosition Hinge position. - * @property orientation Orientation (Horizontal, Vertical) - */ - data class BookPosture( - val hingePosition: Rect, - val orientation: FoldingFeature.Orientation - ) : DevicePosture - - /** - * Separating posture posture of the foldable device. - * - * @author marlonlom - * - * @property hingePosition Hinge position. - * @property orientation Orientation (Horizontal, Vertical) - */ - data class Separating( - val hingePosition: Rect, - var orientation: FoldingFeature.Orientation, - ) : DevicePosture - - companion object { - - /** - * Returns the device posture for selected layout information. - * - * @param layoutInfo Window layout information. - */ - @JvmStatic - fun fromLayoutInfo(layoutInfo: WindowLayoutInfo): DevicePosture { - val foldingFeature = - layoutInfo.displayFeatures.filterIsInstance().firstOrNull() - - return when { - - isBookPosture(foldingFeature) -> BookPosture( - hingePosition = foldingFeature.bounds, - orientation = foldingFeature.orientation - ) - - isSeparating(foldingFeature) -> Separating( - hingePosition = foldingFeature.bounds, - orientation = foldingFeature.orientation - ) - - else -> NormalPosture - } - } - } - -} - -@OptIn(ExperimentalContracts::class) -fun isBookPosture(foldFeature: FoldingFeature?): Boolean { - contract { returns(true) implies (foldFeature != null) } - return foldFeature?.state == FoldingFeature.State.HALF_OPENED -} - -@OptIn(ExperimentalContracts::class) -fun isSeparating(foldFeature: FoldingFeature?): Boolean { - contract { returns(true) implies (foldFeature != null) } - return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating -} diff --git a/apps/mobile-app/src/main/res/values-es/strings.xml b/apps/mobile-app/src/main/res/values-es/strings.xml index e6fd969..8176b8b 100644 --- a/apps/mobile-app/src/main/res/values-es/strings.xml +++ b/apps/mobile-app/src/main/res/values-es/strings.xml @@ -20,7 +20,7 @@ configuraciones de la Aplicación Versión de la aplicación - Favorites + Favoritos Bienvenidos A continuación podrás ver el catálogo de productos del programa Amigos Juan Valdez®, en el cual podrás vivir la experiencia única de visitar las tiendas Juan Valdez® Café utilizando tus puntos para canjearlos por alimentos, bebidas, premios y beneficios. diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt index b5cf3b5..7de790a 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt @@ -17,6 +17,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import java.util.Locale internal class CatalogDetailRepositoryTest { @@ -26,7 +27,7 @@ internal class CatalogDetailRepositoryTest { fun setUp() { repository = CatalogDetailRepository( FakeLocalDataSource( - CatalogDataService() + CatalogDataService(Locale.getDefault().language) ) ) } diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt index 1fe9c9b..f07ea32 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt @@ -13,6 +13,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test +import java.util.Locale internal class CatalogDetailViewModelTest { @@ -25,7 +26,7 @@ internal class CatalogDetailViewModelTest { fun setUp() { viewModel = CatalogDetailViewModel( CatalogDetailRepository( - FakeLocalDataSource(CatalogDataService()) + FakeLocalDataSource(CatalogDataService(Locale.getDefault().language)) ) ) } diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRepositoryTest.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRepositoryTest.kt index 593f6a3..edb37e8 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRepositoryTest.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListRepositoryTest.kt @@ -13,6 +13,7 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Test +import java.util.Locale internal class CatalogListRepositoryTest { @@ -22,9 +23,9 @@ internal class CatalogListRepositoryTest { fun setUp() { repository = CatalogListRepository( localDataSource = FakeLocalDataSource( - CatalogDataService() + CatalogDataService(Locale.getDefault().language) ), - catalogDataService = CatalogDataService() + catalogDataService = CatalogDataService(Locale.getDefault().language) ) } diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListViewModelTest.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListViewModelTest.kt index 3b0b5ee..9501c24 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListViewModelTest.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_list/CatalogListViewModelTest.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Rule import org.junit.Test +import java.util.Locale internal class CatalogListViewModelTest { @@ -25,9 +26,9 @@ internal class CatalogListViewModelTest { viewModel = CatalogListViewModel( CatalogListRepository( localDataSource = FakeLocalDataSource( - CatalogDataService() + CatalogDataService(Locale.getDefault().language) ), - catalogDataService = CatalogDataService() + catalogDataService = CatalogDataService(Locale.getDefault().language) ) ) val uiState = viewModel.uiState diff --git a/features/core/catalog-source/src/main/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataService.kt b/features/core/catalog-source/src/main/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataService.kt index 3e7fe5a..f83a2b5 100644 --- a/features/core/catalog-source/src/main/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataService.kt +++ b/features/core/catalog-source/src/main/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataService.kt @@ -7,16 +7,21 @@ package dev.marlonlom.apps.cappajv.core.catalog_source import kotlinx.serialization.json.Json import java.io.InputStream +import java.util.Locale /** * Catalog data service class. * * @author marlonlom * + * @property language Selected language for fetching catalog data, default to english (en). */ -class CatalogDataService { +class CatalogDataService( + private val language: String = Locale.ENGLISH.language +) { + + private var catalogJsonPath = if (language == "es") CATALOG_JSON_FILENAME else ENG_CATALOG_JSON_FILENAME - private var catalogJsonPath = CATALOG_JSON_FILENAME internal fun changePath(jsonPath: String) { catalogJsonPath = jsonPath } @@ -41,7 +46,8 @@ class CatalogDataService { private fun getJsonResourceAsStream(): InputStream? = this.javaClass.classLoader.getResourceAsStream(catalogJsonPath) companion object { - private const val CATALOG_JSON_FILENAME = "catalog.json" + private const val CATALOG_JSON_FILENAME = "es/catalog.json" + private const val ENG_CATALOG_JSON_FILENAME = "en/catalog.json" } } diff --git a/features/core/catalog-source/src/main/resources/en/catalog.json b/features/core/catalog-source/src/main/resources/en/catalog.json new file mode 100644 index 0000000..e7abc52 --- /dev/null +++ b/features/core/catalog-source/src/main/resources/en/catalog.json @@ -0,0 +1,601 @@ +[ + { + "id": "15398", + "title": "Affogato", + "category": "Cold drinks", + "detail": "It is a perfect Italian coffee-dessert for summer after-dinner meals.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Afogatto-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Affogato", + "pointsQty": 1750 + } + ] + }, + { + "id": "15389", + "title": "Almojábana", + "category": "Pastry", + "detail": "It is a Colombian dough based on corn flour and cheese.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Copia-de-Almojabana-Juan-Valdez-e1642542513402.png", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1400 + } + ] + }, + { + "id": "10411", + "title": "Aromática bosque infusión", + "category": "Hot drinks", + "detail": "It is a hot and very traditional drink in the Colombian Andes, forest infusion flavor.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/AROMATICA.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1475 + }, + { + "label": "Medium", + "pointsQty": 1725 + } + ] + }, + { + "id": "10410", + "title": "Aromática fusión tropical", + "category": "Hot drinks", + "detail": "It is a hot and very traditional drink in the Colombian Andes, tropical fusion flavor.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/TE.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1475 + }, + { + "label": "Medium", + "pointsQty": 1725 + } + ] + }, + { + "id": "970", + "title": "Aromática primavera", + "category": "Hot drinks", + "detail": "It is a hot and very traditional drink in the Colombian Andes, spring flavor.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/TE.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1475 + }, + { + "label": "Medium", + "pointsQty": 1725 + } + ] + }, + { + "id": "971", + "title": "Aromática silvestre", + "category": "Hot drinks", + "detail": "It is a hot and very traditional drink in the Colombian Andes, wild flavor.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/AROMATICA.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1475 + }, + { + "label": "Medium", + "pointsQty": 1725 + } + ] + }, + { + "id": "10419", + "title": "Café americano", + "category": "Hot drinks", + "detail": "Coffee made from espresso and hot water.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Cafe-Americano-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 1725 + } + ] + }, + { + "id": "10418", + "title": "Cappuccino", + "category": "Hot drinks", + "detail": "Espresso coffee with steamed milk with a creamy texture.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Cappuccino-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1625 + }, + { + "label": "Medium", + "pointsQty": 1875 + }, + { + "label": "Medium Decaf", + "pointsQty": 1875 + }, + { + "label": "Big", + "pointsQty": 2375 + } + ] + }, + { + "id": "10412", + "title": "Chai", + "category": "Hot drinks", + "detail": "Hot drink based on spiced tea.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Chai-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Traditional Medium", + "pointsQty": 2725 + }, + { + "label": "Apple Cinnamon Medium", + "pointsQty": 2875 + }, + { + "label": "Large Cinnamon Apple", + "pointsQty": 3125 + }, + { + "label": "Traditional Large 1.65", + "pointsQty": 2975 + } + ] + }, + { + "id": "10416", + "title": "Chocolate", + "category": "Hot drinks", + "detail": "Chocolate with steamed milk.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Chocolate-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 2125 + } + ] + }, + { + "id": "15390", + "title": "Cold brew nitro", + "category": "Cold drinks", + "detail": "Nitrogen foaming coffee after resting in cold water for 24 hours, with a mild flavor, delicious aroma, with the color of natural coffee and a beer-like appearance.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Cold-Brew-nitro-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1475 + } + ] + }, + { + "id": "15391", + "title": "Cold brew original", + "category": "Cold drinks", + "detail": "Coffee rested in cold water for 24 hours, with a mild flavor, delicious aroma and the color of natural coffee.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Cold-Brew-nitro-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1225 + } + ] + }, + { + "id": "10415", + "title": "Espresso", + "category": "Hot drinks", + "detail": "Coffee prepared in espresso machine.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Espresso-tradicional-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Normal", + "pointsQty": 1375 + }, + { + "label": "Double", + "pointsQty": 1725 + }, + { + "label": "chopped up", + "pointsQty": 1975 + } + ] + }, + { + "id": "979", + "title": "Flat white", + "category": "Hot drinks", + "detail": "Coffee prepared in an espresso machine with a thin layer of milk foam.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Flat-white-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 2075 + } + ] + }, + { + "id": "15379", + "title": "Galleta choco chip", + "category": "Pastry", + "detail": "A crunchy cookie with chocolate chips.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Galleta-chips-de-chocolate-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1375 + } + ] + }, + { + "id": "15396", + "title": "Granizado", + "category": "Cold drinks", + "detail": "It is a cold and refreshing iced coffee drink.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Granizado-juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 2225 + } + ] + }, + { + "id": "10417", + "title": "Latte", + "category": "Hot drinks", + "detail": "It is a coffee prepared in an espresso machine and steamed milk.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Latte-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1625 + }, + { + "label": "Medium", + "pointsQty": 1875 + }, + { + "label": "Medium Decaf", + "pointsQty": 1875 + }, + { + "label": "Big", + "pointsQty": 2375 + } + ] + }, + { + "id": "15397", + "title": "Latte frío", + "category": "Cold drinks", + "detail": "It is a coffee prepared in an espresso machine and steamed milk, served cold.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Latte-frio-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 2375 + } + ] + }, + { + "id": "10414", + "title": "Macchiatto", + "category": "Hot drinks", + "detail": "Espresso coffee with a layer of milk foam.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Machiatto-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Macchiatto", + "pointsQty": 1975 + } + ] + }, + { + "id": "983", + "title": "Mocca", + "category": "Hot drinks", + "detail": "Hot drink that combines espresso coffee, milk and chocolate.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Mocca-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 2225 + } + ] + }, + { + "id": "969", + "title": "Nevado arequipe", + "category": "Cold drinks", + "detail": "It is a creamy cold coffee drink with arequipe decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Nevado-Arequipe-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3225 + } + ] + }, + { + "id": "15392", + "title": "Nevado baileys", + "category": "Cold drinks", + "detail": "It is a creamy cold drink based on coffee with Baileys decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Nevado-Baileys-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3625 + } + ] + }, + { + "id": "15381", + "title": "Nevado brownie", + "category": "Cold drinks", + "detail": "It is a creamy cold coffee drink with brownie decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Nevado-Brownie-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3225 + } + ] + }, + { + "id": "12971", + "title": "Nevado cafe", + "category": "Cold drinks", + "detail": "It is a creamy cold coffee-based drink decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2023/02/nevado_de_cafe___300ml_700x700px.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 2975 + } + ] + }, + { + "id": "12981", + "title": "Nevado café reducido en azucar", + "category": "Cold drinks", + "detail": "It is a creamy cold coffee-based drink, reduced in sugar decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2023/02/nevado_rejado_en_azucar_300ml_700x700px.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3225 + } + ] + }, + { + "id": "15393", + "title": "Nevado chai", + "category": "Cold drinks", + "detail": "It is a creamy cold drink based on chai tea decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Nevado-chai-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3475 + } + ] + }, + { + "id": "12982", + "title": "Nevado chocolate", + "category": "Cold drinks", + "detail": "It is a creamy cold chocolate coffee drink decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2023/02/nevado_de_chocolate_300ml_700x700px.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3225 + } + ] + }, + { + "id": "968", + "title": "Nevado galleta", + "category": "Cold drinks", + "detail": "It is a creamy cold coffee drink with cookie pieces and condensed milk decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Nevado-galleta-oreo-y-leche-condensada-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3225 + } + ] + }, + { + "id": "12983", + "title": "Nevado mokachip", + "category": "Cold drinks", + "detail": "It is a creamy cold coffee drink with chocolate and chips decorated with Chantilly.", + "picture": "https://juanvaldez.com/wp-content/uploads/2023/02/nevado_mocachip_300ml_700x700px.jpg", + "punctuations": [ + { + "label": "Medium", + "pointsQty": 3225 + } + ] + }, + { + "id": "15388", + "title": "Palito de queso", + "category": "Pastry", + "detail": "It is a butter-based puff pastry filled with a mixture of peasant cheese, mozarella and coastal cheese.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Palito-de-queso-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1725 + } + ] + }, + { + "id": "15387", + "title": "Pandebono", + "category": "Pastry", + "detail": "It is a dough based on cassava starch and cheese.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Pandebono-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1400 + } + ] + }, + { + "id": "15386", + "title": "Pandebono de bocadillo", + "category": "Pastry", + "detail": "It is a dough based on cassava starch and cheese filled with a sandwich.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/PANDEBONO_DE_BOCADILLO.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1475 + } + ] + }, + { + "id": "15385", + "title": "Pastel gloria arequipe y guayaba", + "category": "Pastry", + "detail": "It is a puff pastry filled with arequipe and guava.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Pastel-gloria-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1375 + } + ] + }, + { + "id": "10413", + "title": "Pods", + "category": "Hot drinks", + "detail": "It is prepared in a special machine for Pods.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/pods.png", + "punctuations": [ + { + "label": "Hill", + "pointsQty": 1500 + }, + { + "label": "Summit", + "pointsQty": 1500 + }, + { + "label": "Summit Decaf", + "pointsQty": 1500 + }, + { + "label": "Solid", + "pointsQty": 1500 + }, + { + "label": "Origin (Huila, Nariño, Sierra, Tolima)", + "pointsQty": 1625 + } + ] + }, + { + "id": "12984", + "title": "Rollito de canela", + "category": "Pastry", + "detail": "It is a sweet bread with a soft filling of cinnamon, butter and freshly baked sugar.", + "picture": "https://juanvaldez.com/wp-content/uploads/2023/02/rollito_de_canela.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1725 + } + ] + }, + { + "id": "987", + "title": "Tintos", + "category": "Hot drinks", + "detail": "Cup of filtered coffee flavored with panela, cloves, cinnamon and lemon.", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Tinto-Juan-Valdez-campesino.jpg", + "punctuations": [ + { + "label": "Small", + "pointsQty": 1125 + }, + { + "label": "Medium", + "pointsQty": 1325 + }, + { + "label": "Big", + "pointsQty": 1575 + }, + { + "label": "Small Campesino", + "pointsQty": 1375 + }, + { + "label": "Medium Campesino", + "pointsQty": 1575 + } + ] + }, + { + "id": "15384", + "title": "Torta de banano", + "category": "Pastry", + "detail": "Delicious slice of banana cake", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Torta-de-banano-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 1875 + } + ] + }, + { + "id": "15383", + "title": "Torta de chocolate", + "category": "Pastry", + "detail": "Delicious slice of chocolate cake", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Torta-de-chocolate-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 2225 + } + ] + }, + { + "id": "15382", + "title": "Torta de zanahoria", + "category": "Pastry", + "detail": "Delicious slice of carrot cake", + "picture": "https://juanvaldez.com/wp-content/uploads/2022/10/Torta-de-zanahoria-Juan-Valdez.jpg", + "punctuations": [ + { + "label": "Unit", + "pointsQty": 2225 + } + ] + } +] diff --git a/features/core/catalog-source/src/main/resources/catalog.json b/features/core/catalog-source/src/main/resources/es/catalog.json similarity index 100% rename from features/core/catalog-source/src/main/resources/catalog.json rename to features/core/catalog-source/src/main/resources/es/catalog.json diff --git a/features/core/catalog-source/src/test/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataServiceTest.kt b/features/core/catalog-source/src/test/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataServiceTest.kt index 0d1b791..824b9bf 100644 --- a/features/core/catalog-source/src/test/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataServiceTest.kt +++ b/features/core/catalog-source/src/test/kotlin/dev/marlonlom/apps/cappajv/core/catalog_source/CatalogDataServiceTest.kt @@ -19,7 +19,7 @@ internal class CatalogDataServiceTest { @Before fun init() { - catalogResponse = CatalogDataService().fetchData() + catalogResponse = CatalogDataService("es").fetchData() } @Test @@ -71,7 +71,7 @@ internal class CatalogDataServiceTest { @Test fun shouldValidateErrorFetchingWrongJsonData() { - val service = CatalogDataService() + val service = CatalogDataService("es") service.changePath("none.json") catalogResponse = service.fetchData() assertTrue(catalogResponse is Response.Failure) @@ -81,7 +81,7 @@ internal class CatalogDataServiceTest { @Test fun shouldValidateErrorWhileSerializingJsonData() { - val service = CatalogDataService() + val service = CatalogDataService("es") service.changePath("catalog-single.json") catalogResponse = service.fetchData() assertTrue(catalogResponse is Response.Failure) diff --git a/features/core/database/build.gradle.kts b/features/core/database/build.gradle.kts index 9557518..7710f87 100644 --- a/features/core/database/build.gradle.kts +++ b/features/core/database/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) implementation(libs.bundles.database.room) + implementation(libs.google.guava) ksp(libs.androidx.room.compiler) diff --git a/features/core/preferences-datastore/build.gradle.kts b/features/core/preferences-datastore/build.gradle.kts index 5685cc7..381411b 100644 --- a/features/core/preferences-datastore/build.gradle.kts +++ b/features/core/preferences-datastore/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.core) + implementation(libs.google.guava) testImplementation(libs.junit) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e4c627..e5d5389 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ androidx-room-ktx = "androidx.room:room-ktx:2.6.1" androidx-room-runtime = "androidx.room:room-runtime:2.6.1" androidx-window = "androidx.window:window:1.2.0" coil-compose = "io.coil-kt:coil-compose:2.6.0" +google-guava = "com.google.guava:guava:27.0.1-android" google-oss-licenses = "com.google.android.gms:play-services-oss-licenses:17.0.1" google-oss-licenses-plugin = "com.google.android.gms:oss-licenses-plugin:0.10.6" kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22" @@ -28,6 +29,11 @@ kotlinx-coroutines-core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0" kotlinx-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" jakewharton-timber = "com.jakewharton.timber:timber:5.0.1" +# koin-implementation-with-bom +koin-bom = "io.insert-koin:koin-bom:3.5.0" +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" } +koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation" } + # test-implementation junit = "junit:junit:4.13.2" kotlinx-coroutines-test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" @@ -46,7 +52,6 @@ androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-material3-wsc = { module = "androidx.compose.material3:material3-window-size-class" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } -androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }