From b30ea5d31d5363e7fd6a23ef55090a8495052eb6 Mon Sep 17 00:00:00 2001 From: michaelbel Date: Wed, 18 Dec 2024 17:23:12 +0300 Subject: [PATCH] Update project --- .github/data/data.json | 12 + .../org/michaelbel/core/ktx/ModifierKtx.kt | 7 + .../core/ktx/WindowAdaptiveInfoKtx.kt | 4 +- mobile/build.gradle.kts | 1 + .../org/michaelbel/template/AppModule.kt | 4 +- .../org/michaelbel/template/MainActivity.kt | 5 +- .../template/ui/MainActivityContent.kt | 647 +++++++++--------- .../org/michaelbel/template/ui/Navigation.kt | 20 - .../michaelbel/template/ui/TabNavigation.kt | 29 + .../template/ui/about/AboutScreen.kt | 45 ++ .../template/ui/details/DetailsScreen.kt | 103 ++- .../template/ui/details2/DetailsScreen2.kt | 70 ++ .../template/ui/details2/DetailsViewModel2.kt | 28 + .../ui/details2/empty/DetailsEmptyScreen.kt | 33 + .../michaelbel/template/ui/list/ListScreen.kt | 56 +- .../template/ui/settings/SettingsScreen.kt | 45 ++ 16 files changed, 722 insertions(+), 387 deletions(-) delete mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/Navigation.kt create mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt create mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/about/AboutScreen.kt create mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt create mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt create mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt create mode 100644 mobile/src/main/kotlin/org/michaelbel/template/ui/settings/SettingsScreen.kt diff --git a/.github/data/data.json b/.github/data/data.json index 9e2d7ac6..2a1cdbdf 100644 --- a/.github/data/data.json +++ b/.github/data/data.json @@ -16,5 +16,17 @@ "name": "Central Asian wild boar", "description": "The Central Asian wild boar (Sus scrofa nigripes) is a subspecies of wild boar native to Central Asia, including Kazakhstan, Uzbekistan, and surrounding regions. It has a lighter build compared to its Siberian relatives, with shorter fur and well-developed tusks. Adapted to arid and semi-arid environments, it feeds on roots, plants, and small animals, displaying remarkable endurance in harsh conditions.", "picture": "https://i.ibb.co/xfBHnH7/central-asian-boar.webp" + }, + { + "id": 4, + "name": "Indian wild boar.", + "description": "The Indian wild boar (*Sus scrofa cristatus*) is a subspecies of wild pig native to South Asia. It has a robust body, dark gray to black coloration, and a distinctive mane of coarse hair along its back. Found in forests, grasslands, and agricultural areas, it is known for its adaptability and plays a key role in the ecosystem as a forager.", + "picture": "https://i.ibb.co/SNdTNcW/Indian-wild-boar.webp" + }, + { + "id": 5, + "name": "Pygmy hog", + "description": "The pygmy hog (*Porcula salvania*) is the smallest and rarest wild pig, found in the grasslands of northeastern India and Nepal. It is critically endangered, with a compact body, dark brown coloration, and short legs, adapted to dense vegetation.", + "picture": "https://i.ibb.co/kBmgqJg/pygmy-hog.webp" } ] \ No newline at end of file diff --git a/core/src/main/kotlin/org/michaelbel/core/ktx/ModifierKtx.kt b/core/src/main/kotlin/org/michaelbel/core/ktx/ModifierKtx.kt index a0b84433..92f397ca 100644 --- a/core/src/main/kotlin/org/michaelbel/core/ktx/ModifierKtx.kt +++ b/core/src/main/kotlin/org/michaelbel/core/ktx/ModifierKtx.kt @@ -4,6 +4,8 @@ package org.michaelbel.core.ktx import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -16,4 +18,9 @@ fun Modifier.clickableWithoutRipple( indication = null, onClick = { block() } ) +} + +@Composable +fun Modifier.displayCutoutPaddingIfLandscape(): Modifier { + return if (isLandscape) displayCutoutPadding() else this } \ No newline at end of file diff --git a/core/src/main/kotlin/org/michaelbel/core/ktx/WindowAdaptiveInfoKtx.kt b/core/src/main/kotlin/org/michaelbel/core/ktx/WindowAdaptiveInfoKtx.kt index e093ec27..0f1e8204 100644 --- a/core/src/main/kotlin/org/michaelbel/core/ktx/WindowAdaptiveInfoKtx.kt +++ b/core/src/main/kotlin/org/michaelbel/core/ktx/WindowAdaptiveInfoKtx.kt @@ -15,8 +15,8 @@ inline val navigationSuiteType: NavigationSuiteType val density = LocalDensity.current val windowSize = with(density) { currentWindowSize().toSize().toDpSize() } return when { - adaptiveInfo.windowPosture.isTabletop -> NavigationSuiteType.NavigationBar - adaptiveInfo.windowSizeClass.isCompact -> NavigationSuiteType.NavigationBar + adaptiveInfo.windowPosture.isTabletop && isPortrait -> NavigationSuiteType.NavigationBar + adaptiveInfo.windowSizeClass.isCompact && isPortrait -> NavigationSuiteType.NavigationBar adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED && windowSize.width >= 1200.dp -> NavigationSuiteType.NavigationDrawer else -> NavigationSuiteType.NavigationRail } diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 12ff1b5d..1c27ac7c 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.parcelize) } private val gitCommitsCount: Int by lazy { diff --git a/mobile/src/main/kotlin/org/michaelbel/template/AppModule.kt b/mobile/src/main/kotlin/org/michaelbel/template/AppModule.kt index 46d8e387..448ff0c8 100644 --- a/mobile/src/main/kotlin/org/michaelbel/template/AppModule.kt +++ b/mobile/src/main/kotlin/org/michaelbel/template/AppModule.kt @@ -25,8 +25,9 @@ import org.michaelbel.template.ktor.AppService import org.michaelbel.template.repository.AppRepository import org.michaelbel.template.room.AppDao import org.michaelbel.template.room.AppDatabase -import org.michaelbel.template.ui.list.ListViewModel import org.michaelbel.template.ui.details.DetailsViewModel +import org.michaelbel.template.ui.details2.DetailsViewModel2 +import org.michaelbel.template.ui.list.ListViewModel val appModule = module { includes(dispatchersKoinModule) @@ -84,4 +85,5 @@ val appModule = module { viewModelOf(::MainViewModel) viewModelOf(::ListViewModel) viewModelOf(::DetailsViewModel) + viewModelOf(::DetailsViewModel2) } \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/MainActivity.kt b/mobile/src/main/kotlin/org/michaelbel/template/MainActivity.kt index 1c20a36e..1cb2f062 100644 --- a/mobile/src/main/kotlin/org/michaelbel/template/MainActivity.kt +++ b/mobile/src/main/kotlin/org/michaelbel/template/MainActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import org.michaelbel.template.ui.AppTheme import org.michaelbel.template.ui.MainActivityContent class MainActivity: AppCompatActivity() { @@ -14,7 +15,9 @@ class MainActivity: AppCompatActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - MainActivityContent() + AppTheme { + MainActivityContent() + } } } } \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt index 93adf923..a30564f8 100644 --- a/mobile/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) package org.michaelbel.template.ui @@ -9,15 +9,14 @@ import androidx.compose.foundation.background 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.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -50,8 +49,12 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType import androidx.compose.runtime.Composable @@ -78,18 +81,23 @@ import androidx.navigation.compose.rememberNavController import androidx.window.core.layout.WindowHeightSizeClass import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel -import org.michaelbel.core.ktx.isLandscape +import org.michaelbel.core.ktx.displayCutoutPaddingIfLandscape +import org.michaelbel.core.ktx.isPortrait import org.michaelbel.core.ktx.navigationSuiteType import org.michaelbel.template.MainViewModel +import org.michaelbel.template.ui.about.AboutScreen import org.michaelbel.template.ui.details.DetailsScreen +import org.michaelbel.template.ui.details2.DetailsScreen2 +import org.michaelbel.template.ui.details2.empty.DetailsEmptyScreen import org.michaelbel.template.ui.list.ListScreen +import org.michaelbel.template.ui.settings.SettingsScreen @Composable fun MainActivityContent( viewModel: MainViewModel = koinViewModel() ) { val navHostController = rememberNavController() - var selectedRoute by remember { mutableStateOf(Navigation.Home) } + var selectedTabRoute by remember { mutableStateOf(TabNavigation.Home) } val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() @@ -102,380 +110,377 @@ fun MainActivityContent( } val layoutDirection = LocalLayoutDirection.current - AppTheme { - NavigationSuiteScaffoldLayout( - navigationSuite = { - when (navigationSuiteType) { - NavigationSuiteType.NavigationBar -> { - NavigationBar( - modifier = Modifier.fillMaxWidth() - ) { - NavigationBarItem( - selected = selectedRoute == Navigation.Home, - onClick = { selectedRoute = Navigation.Home }, - icon = { - Icon( - imageVector = Icons.Outlined.Home, - contentDescription = null - ) - }, - label = { - Text( - text = "Home" - ) - } - ) - - NavigationBarItem( - selected = selectedRoute == Navigation.Settings, - onClick = { selectedRoute = Navigation.Settings }, - icon = { - Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = null - ) - }, - label = { - Text( - text = "Settings" - ) - } - ) + val listDetailPaneScaffoldNavigator = rememberListDetailPaneScaffoldNavigator() - NavigationBarItem( - selected = selectedRoute == Navigation.About, - onClick = { selectedRoute = Navigation.About }, - icon = { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = null - ) - }, - label = { - Text( - text = "About" - ) - } - ) - } - } - NavigationSuiteType.NavigationRail -> { - NavigationRail( - modifier = Modifier.fillMaxHeight(), - containerColor = MaterialTheme.colorScheme.inverseOnSurface - ) { - Column( - modifier = Modifier.layoutId(LayoutType.HEADER), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) + NavHost( + navController = navHostController, + startDestination = AppNavigation.Main, + modifier = Modifier.fillMaxSize() + ) { + composable { + NavigationSuiteScaffoldLayout( + navigationSuite = { + when (navigationSuiteType) { + NavigationSuiteType.NavigationBar -> { + NavigationBar( + modifier = Modifier.fillMaxWidth() ) { - FloatingActionButton( - onClick = { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "Single-line snackbar with action", - actionLabel = "Action", - duration = SnackbarDuration.Short - ) - } - }, - modifier = Modifier - .statusBarsPadding() - .padding(top = 16.dp), - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = null - ) - } - - Spacer(Modifier.height(16.dp)) - - NavigationRailItem( - selected = selectedRoute == Navigation.Home, - onClick = { selectedRoute = Navigation.Home }, + NavigationBarItem( + selected = selectedTabRoute == TabNavigation.Home, + onClick = { selectedTabRoute = TabNavigation.Home }, icon = { Icon( imageVector = Icons.Outlined.Home, contentDescription = null ) + }, + label = { + Text( + text = "Home" + ) } ) - NavigationRailItem( - selected = selectedRoute == Navigation.About, - onClick = { selectedRoute = Navigation.About }, + NavigationBarItem( + selected = selectedTabRoute == TabNavigation.Settings, + onClick = { selectedTabRoute = TabNavigation.Settings }, icon = { - BadgedBox( - badge = { - this@Column.AnimatedVisibility( - visible = selectedRoute != Navigation.About, - enter = fadeIn(), - exit = fadeOut() - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(24.dp) - .background(color = Color.Red, shape = CircleShape) - ) { - Text( - text = "12", - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.Medium - ) - } - } - } - ) { - Icon( - imageVector = Icons.Outlined.Email, - contentDescription = null - ) - } + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = null + ) + }, + label = { + Text( + text = "Settings" + ) } ) - NavigationRailItem( - selected = selectedRoute == Navigation.Settings, - onClick = { selectedRoute = Navigation.Settings }, + NavigationBarItem( + selected = selectedTabRoute == TabNavigation.About, + onClick = { selectedTabRoute = TabNavigation.About }, icon = { Icon( - imageVector = Icons.Outlined.Settings, + imageVector = Icons.Outlined.Info, contentDescription = null ) + }, + label = { + Text( + text = "About" + ) } ) } } - } - NavigationSuiteType.NavigationDrawer -> { - PermanentDrawerSheet( - modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp), - drawerContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ) { - Layout( + NavigationSuiteType.NavigationRail -> { + NavigationRail( modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(16.dp), - content = { - Column( - modifier = Modifier.layoutId(LayoutType.HEADER), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(4.dp) + .displayCutoutPaddingIfLandscape() + .fillMaxHeight(), + containerColor = MaterialTheme.colorScheme.inverseOnSurface + ) { + Column( + modifier = Modifier.layoutId(LayoutType.HEADER), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + FloatingActionButton( + onClick = { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Single-line snackbar with action", + actionLabel = "Action", + duration = SnackbarDuration.Short + ) + } + }, + modifier = Modifier + .statusBarsPadding() + .padding(top = 16.dp), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer ) { - ExtendedFloatingActionButton( - onClick = { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "Single-line snackbar with action", - actionLabel = "Action", - duration = SnackbarDuration.Short - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .statusBarsPadding(), - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null + ) + } + + Spacer(Modifier.height(16.dp)) + + NavigationRailItem( + selected = selectedTabRoute == TabNavigation.Home, + onClick = { selectedTabRoute = TabNavigation.Home }, + icon = { Icon( - imageVector = Icons.Default.Edit, - contentDescription = null, - modifier = Modifier.size(24.dp) + imageVector = Icons.Outlined.Home, + contentDescription = null ) + } + ) - Text( - text = "Compose", - modifier = Modifier.padding(start = 8.dp), + NavigationRailItem( + selected = selectedTabRoute == TabNavigation.Settings, + onClick = { selectedTabRoute = TabNavigation.Settings }, + icon = { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = null ) } - } + ) - Column( - modifier = Modifier - .layoutId(LayoutType.CONTENT) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - NavigationDrawerItem( - selected = selectedRoute == Navigation.Home, - onClick = { selectedRoute = Navigation.Home }, - icon = { + NavigationRailItem( + selected = selectedTabRoute == TabNavigation.About, + onClick = { selectedTabRoute = TabNavigation.About }, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null + ) + } + ) + } + } + } + NavigationSuiteType.NavigationDrawer -> { + PermanentDrawerSheet( + modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp), + drawerContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Layout( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(16.dp), + content = { + Column( + modifier = Modifier.layoutId(LayoutType.HEADER), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + ExtendedFloatingActionButton( + onClick = { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Single-line snackbar with action", + actionLabel = "Action", + duration = SnackbarDuration.Short + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding(), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer + ) { Icon( - imageVector = Icons.Outlined.Home, - contentDescription = null + imageVector = Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(24.dp) ) - }, - label = { + Text( - text = "Home" + text = "Compose", + modifier = Modifier.padding(start = 8.dp), ) } - ) + } + + Column( + modifier = Modifier + .layoutId(LayoutType.CONTENT) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + NavigationDrawerItem( + selected = selectedTabRoute == TabNavigation.Home, + onClick = { selectedTabRoute = TabNavigation.Home }, + icon = { + Icon( + imageVector = Icons.Outlined.Home, + contentDescription = null + ) + }, + label = { + Text( + text = "Home" + ) + } + ) - NavigationDrawerItem( - selected = selectedRoute == Navigation.About, - onClick = { selectedRoute = Navigation.About }, - icon = { - BadgedBox( - badge = { - this@Column.AnimatedVisibility( - visible = selectedRoute != Navigation.About, - enter = fadeIn(), - exit = fadeOut() - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(24.dp) - .background(color = Color.Red, shape = CircleShape) + NavigationDrawerItem( + selected = selectedTabRoute == TabNavigation.About, + onClick = { selectedTabRoute = TabNavigation.About }, + icon = { + BadgedBox( + badge = { + this@Column.AnimatedVisibility( + visible = selectedTabRoute != TabNavigation.About, + enter = fadeIn(), + exit = fadeOut() ) { - Text( - text = "12", - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.Medium - ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(24.dp) + .background(color = Color.Red, shape = CircleShape) + ) { + Text( + text = "12", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.Medium + ) + } } } + ) { + Icon( + imageVector = Icons.Outlined.Email, + contentDescription = null + ) } - ) { + }, + label = { + Text( + text = "Chat" + ) + } + ) + + NavigationDrawerItem( + selected = selectedTabRoute == TabNavigation.Settings, + onClick = { selectedTabRoute = TabNavigation.Settings }, + icon = { Icon( - imageVector = Icons.Outlined.Email, + imageVector = Icons.Outlined.Settings, contentDescription = null ) + }, + label = { + Text( + text = "Settings" + ) } - }, - label = { - Text( - text = "Chat" - ) - } - ) - - NavigationDrawerItem( - selected = selectedRoute == Navigation.Settings, - onClick = { selectedRoute = Navigation.Settings }, - icon = { - Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = null - ) - }, - label = { - Text( - text = "Settings" - ) - } + ) + } + }, + measurePolicy = navigationMeasurePolicy(navContentPosition) + ) + } + } + } + }, + layoutType = navigationSuiteType + ) { + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState + ) + }, + floatingActionButton = { + if (navigationSuiteType == NavigationSuiteType.NavigationBar) { + ExtendedFloatingActionButton( + onClick = { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Single-line snackbar with action", + actionLabel = "Action", + duration = SnackbarDuration.Short ) } }, - measurePolicy = navigationMeasurePolicy(navContentPosition) - ) + modifier = Modifier.offset(y = 16.dp), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer + ) { + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = null + ) + + Text( + text = "Compose", + modifier = Modifier.padding(start = 8.dp) + ) + } } } - } - }, - layoutType = navigationSuiteType - ) { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - title = { - Text( - text = "Android Template" + ) { innerPadding -> + val tabsNavHostController = rememberNavController() + + NavHost( + navController = tabsNavHostController, + startDestination = selectedTabRoute, + modifier = Modifier + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + top = 0.dp, + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = 0.dp ) - } - ) - }, - snackbarHost = { - SnackbarHost( - hostState = snackbarHostState - ) - }, - floatingActionButton = { - if (navigationSuiteType == NavigationSuiteType.NavigationBar) { - ExtendedFloatingActionButton( - onClick = { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = "Single-line snackbar with action", - actionLabel = "Action", - duration = SnackbarDuration.Short + .fillMaxSize() + ) { + composable { + when { + isPortrait -> { + ListScreen( + onClick = { navHostController.navigate(AppNavigation.Details(it)) } ) } - }, - modifier = Modifier.offset(y = 16.dp), - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ) { - Icon( - imageVector = Icons.Outlined.Edit, - contentDescription = null - ) - - Text( - text = "Compose", - modifier = Modifier.padding(start = 8.dp) - ) + else -> { + ListDetailPaneScaffold( + directive = listDetailPaneScaffoldNavigator.scaffoldDirective, + value = listDetailPaneScaffoldNavigator.scaffoldValue, + listPane = { + AnimatedPane( + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(0.4F) + ) { + ListScreen( + onClick = { listDetailPaneScaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, AppNavigation.Details(it)) } + ) + } + }, + detailPane = { + AnimatedPane( + modifier = Modifier.fillMaxWidth(0.6F) + ) { + when { + listDetailPaneScaffoldNavigator.currentDestination?.content != null -> { + DetailsScreen2( + id = listDetailPaneScaffoldNavigator.currentDestination?.content?.id!! + ) + } + else -> { + DetailsEmptyScreen() + } + } + } + } + ) + } + } } - } - } - ) { innerPadding -> - NavHost( - navController = navHostController, - startDestination = selectedRoute, - modifier = Modifier - .padding( - start = innerPadding.calculateStartPadding(layoutDirection), - top = innerPadding.calculateTopPadding(), - end = innerPadding.calculateEndPadding(layoutDirection), - bottom = 0.dp - ) - .fillMaxSize() - ) { - composable { - ListScreen( - onClick = { navHostController.navigate(Navigation.Details(it)) }, - modifier = if (isLandscape) Modifier.displayCutoutPadding() else Modifier - ) - } - composable { - DetailsScreen( - modifier = if (isLandscape) Modifier.displayCutoutPadding() else Modifier - ) - } - composable { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Settings" - ) + composable { + SettingsScreen() } - } - composable { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "About" - ) + composable { + AboutScreen() } } } } } + composable { + DetailsScreen() + } } } diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/Navigation.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/Navigation.kt deleted file mode 100644 index e293407b..00000000 --- a/mobile/src/main/kotlin/org/michaelbel/template/ui/Navigation.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.michaelbel.template.ui - -import kotlinx.serialization.Serializable - -sealed interface Navigation { - - @Serializable - data object Home: Navigation - - @Serializable - data class Details( - val id: Int - ): Navigation - - @Serializable - data object Settings: Navigation - - @Serializable - data object About: Navigation -} \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt new file mode 100644 index 00000000..bbf5244e --- /dev/null +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt @@ -0,0 +1,29 @@ +package org.michaelbel.template.ui + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +sealed interface AppNavigation { + + @Serializable + data object Main: AppNavigation + + @Parcelize + @Serializable + data class Details( + val id: Int + ): AppNavigation, Parcelable +} + +sealed interface TabNavigation { + + @Serializable + data object Home: TabNavigation + + @Serializable + data object Settings: TabNavigation + + @Serializable + data object About: TabNavigation +} \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/about/AboutScreen.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/about/AboutScreen.kt new file mode 100644 index 00000000..d959abc4 --- /dev/null +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/about/AboutScreen.kt @@ -0,0 +1,45 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package org.michaelbel.template.ui.about + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun AboutScreen( + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text( + text = "About" + ) + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "About Content" + ) + } + } +} \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/details/DetailsScreen.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/details/DetailsScreen.kt index 2d3e43bf..449b43af 100644 --- a/mobile/src/main/kotlin/org/michaelbel/template/ui/details/DetailsScreen.kt +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/details/DetailsScreen.kt @@ -1,11 +1,16 @@ package org.michaelbel.template.ui.details 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.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -15,6 +20,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import org.koin.androidx.compose.koinViewModel +import org.michaelbel.core.ktx.displayCutoutPaddingIfLandscape +import org.michaelbel.core.ktx.isPortrait @Composable fun DetailsScreen( @@ -23,32 +30,78 @@ fun DetailsScreen( ) { val appEntity by viewModel.appEntity.collectAsStateWithLifecycle() - Column( - modifier = modifier - .fillMaxSize() - .padding(bottom = 16.dp) - ) { - AsyncImage( - model = appEntity.picture, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(220.dp) - ) + Scaffold( + modifier = modifier.fillMaxSize() + ) { innerPadding -> + when { + isPortrait -> { + Column( + modifier = Modifier + .padding(innerPadding) + .displayCutoutPaddingIfLandscape() + .fillMaxSize() + ) { + AsyncImage( + model = appEntity.picture, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + ) - Text( - text = appEntity.name, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp) - ) + Text( + text = appEntity.name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp) + ) - Text( - text = appEntity.description, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) + Text( + text = appEntity.description, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } + else -> { + Row( + modifier = Modifier + .padding(innerPadding) + .displayCutoutPaddingIfLandscape() + .fillMaxSize() + ) { + AsyncImage( + model = appEntity.picture, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth(0.5F) + .fillMaxHeight(0.7F) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Text( + text = appEntity.name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Text( + text = appEntity.description, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 16.dp) + ) + } + } + } + } } } \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt new file mode 100644 index 00000000..d4f2e461 --- /dev/null +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt @@ -0,0 +1,70 @@ +package org.michaelbel.template.ui.details2 + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import org.koin.androidx.compose.koinViewModel + +@Composable +fun DetailsScreen2( + id: Int, + modifier: Modifier = Modifier, + viewModel: DetailsViewModel2 = koinViewModel() +) { + val appEntity by viewModel.appEntity.collectAsStateWithLifecycle() + + LaunchedEffect(id) { + viewModel.idFlow.value = id + } + + Scaffold( + modifier = modifier.fillMaxSize() + ) { innerPadding -> + LazyColumn( + modifier = modifier + .padding(innerPadding) + .padding(end = 16.dp) + .fillMaxSize() + ) { + item { + AsyncImage( + model = appEntity.picture, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + ) + } + item { + Text( + text = appEntity.name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(top = 16.dp) + ) + } + item { + Text( + text = appEntity.description, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt new file mode 100644 index 00000000..7ede58af --- /dev/null +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt @@ -0,0 +1,28 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package org.michaelbel.template.ui.details2 + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.stateIn +import org.michaelbel.core.viewmodel.BaseViewModel +import org.michaelbel.template.interactor.AppInteractor +import org.michaelbel.template.room.AppEntity + +class DetailsViewModel2( + appInteractor: AppInteractor +): BaseViewModel() { + + var idFlow = MutableStateFlow(0) + + val appEntity: StateFlow = idFlow.flatMapLatest { + appInteractor.entityFlow(it) + }.stateIn( + scope = this, + started = SharingStarted.Lazily, + initialValue = AppEntity.Empty + ) +} \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt new file mode 100644 index 00000000..e22cb6ea --- /dev/null +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt @@ -0,0 +1,33 @@ +package org.michaelbel.template.ui.details2.empty + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun DetailsEmptyScreen( + modifier: Modifier = Modifier, +) { + + Scaffold( + modifier = modifier.fillMaxSize() + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Empty" + ) + } + } +} \ No newline at end of file diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt index a4947577..bc90b383 100644 --- a/mobile/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt @@ -1,9 +1,13 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package org.michaelbel.template.ui.list import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -15,14 +19,17 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -39,26 +46,41 @@ fun ListScreen( viewModel: ListViewModel = koinViewModel() ) { val entities by viewModel.appEntities.collectAsStateWithLifecycle() - val configuration = LocalConfiguration.current - val columns = if (configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) { - GridCells.Fixed(1) - } else { - GridCells.Fixed(2) - } + val layoutDirection = LocalLayoutDirection.current - LazyVerticalGrid( - columns = columns, + Scaffold( modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(8.dp) - ) { - items(entities) { entity -> - ListElement( - entity = entity, - onCLick = onClick + topBar = { + TopAppBar( + title = { + Text( + text = "Mobile Template" + ) + } ) } + ) { innerPadding -> + LazyVerticalGrid( + columns = GridCells.Fixed(1), + modifier = Modifier + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + top = innerPadding.calculateTopPadding(), + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = 0.dp + ) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(8.dp) + ) { + items(entities) { entity -> + ListElement( + entity = entity, + onCLick = onClick + ) + } + } } } diff --git a/mobile/src/main/kotlin/org/michaelbel/template/ui/settings/SettingsScreen.kt b/mobile/src/main/kotlin/org/michaelbel/template/ui/settings/SettingsScreen.kt new file mode 100644 index 00000000..abcbc320 --- /dev/null +++ b/mobile/src/main/kotlin/org/michaelbel/template/ui/settings/SettingsScreen.kt @@ -0,0 +1,45 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package org.michaelbel.template.ui.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun SettingsScreen( + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text( + text = "Settings" + ) + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Settings Content" + ) + } + } +} \ No newline at end of file