From 9649a2abee96b14dc2ce86620cbf8e0e5ba9551a Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sat, 17 Feb 2024 12:26:56 +0000 Subject: [PATCH] Added Pages sample tab --- sample/app-desktop/.gitignore | 3 +- .../sample/shared/pages/PagesContent.kt | 70 +++++++++++++++++++ .../sample/shared/root/RootContent.kt | 21 +++++- .../arkivanov/sample/shared/root/RootView.kt | 7 +- .../shared/pages/DefaultPagesComponent.kt | 44 ++++++++++++ .../sample/shared/pages/PagesComponent.kt | 16 +++++ .../shared/root/DefaultRootComponent.kt | 13 ++++ .../shared/root/PreviewRootComponent.kt | 1 + .../sample/shared/root/RootComponent.kt | 3 + .../sample/shared/root/RootContent.kt | 13 +++- 10 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesContent.kt create mode 100644 sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt create mode 100644 sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt diff --git a/sample/app-desktop/.gitignore b/sample/app-desktop/.gitignore index 42afabfd2..002f74ae3 100644 --- a/sample/app-desktop/.gitignore +++ b/sample/app-desktop/.gitignore @@ -1 +1,2 @@ -/build \ No newline at end of file +/build +saved_state.dat diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesContent.kt new file mode 100644 index 000000000..105105b9e --- /dev/null +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesContent.kt @@ -0,0 +1,70 @@ +package com.arkivanov.sample.shared.pages + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetbrains.pages.Pages +import com.arkivanov.decompose.extensions.compose.jetbrains.pages.PagesScrollAnimation +import com.arkivanov.sample.shared.customnavigation.KittenContent + +@OptIn(ExperimentalFoundationApi::class, ExperimentalDecomposeApi::class) +@Composable +fun PagesContent(component: PagesComponent, modifier: Modifier = Modifier) { + Box(modifier = modifier) { + Pages( + pages = component.pages, + onPageSelected = component::selectPage, + modifier = Modifier.fillMaxSize(), + scrollAnimation = PagesScrollAnimation.Default, + ) { _, page -> + KittenContent( + component = page, + textStyle = MaterialTheme.typography.h6, + modifier = Modifier.fillMaxSize(), + ) + } + + Row( + modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedButton( + onClick = component::selectPrev, + modifier = Modifier.size(48.dp), + contentPadding = PaddingValues(0.dp), + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Previous", + ) + } + + OutlinedButton( + onClick = component::selectNext, + modifier = Modifier.size(48.dp), + contentPadding = PaddingValues(0.dp), + ) { + Icon( + imageVector = Icons.Default.ArrowForward, + contentDescription = "Next", + ) + } + } + } +} diff --git a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt index 4d7128666..62e8c0340 100644 --- a/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt +++ b/sample/shared/compose/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt @@ -20,10 +20,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.Direction import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation @@ -40,12 +42,15 @@ import com.arkivanov.sample.shared.customnavigation.CustomNavigationComponent import com.arkivanov.sample.shared.customnavigation.CustomNavigationContent import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeaturesContent import com.arkivanov.sample.shared.multipane.MultiPaneContent +import com.arkivanov.sample.shared.pages.PagesComponent +import com.arkivanov.sample.shared.pages.PagesContent import com.arkivanov.sample.shared.root.RootComponent.Child import com.arkivanov.sample.shared.root.RootComponent.Child.CardsChild import com.arkivanov.sample.shared.root.RootComponent.Child.CountersChild import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.MultiPaneChild +import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild @Composable fun RootContent(component: RootComponent, modifier: Modifier = Modifier) { @@ -78,7 +83,8 @@ private fun Children(component: RootComponent, modifier: Modifier = Modifier) { is CardsChild -> CardsContent(component = child.component, modifier = Modifier.fillMaxSize()) is MultiPaneChild -> MultiPaneContent(component = child.component, modifier = Modifier.fillMaxSize()) is DynamicFeaturesChild -> DynamicFeaturesContent(component = child.component, modifier = Modifier.fillMaxSize()) - is CustomNavigationChild -> CustomNavigationContent(component = child.component, Modifier.fillMaxSize()) + is CustomNavigationChild -> CustomNavigationContent(component = child.component, modifier = Modifier.fillMaxSize()) + is PagesChild -> PagesContent(component = child.component, modifier = Modifier.fillMaxSize()) } } } @@ -143,6 +149,18 @@ private fun BottomBar(component: RootComponent, modifier: Modifier = Modifier) { ) }, ) + + BottomNavigationItem( + selected = activeComponent is PagesComponent, + onClick = component::onPagesTabClicked, + icon = { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Pages", + modifier = Modifier.rotate(90F), + ) + }, + ) } } @@ -163,6 +181,7 @@ private val Child.index: Int is MultiPaneChild -> 2 is DynamicFeaturesChild -> 3 is CustomNavigationChild -> 4 + is PagesChild -> 5 } private fun StackAnimator.flipSide(): StackAnimator = diff --git a/sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/root/RootView.kt b/sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/root/RootView.kt index aabf1d599..ae32d1b79 100644 --- a/sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/root/RootView.kt +++ b/sample/shared/shared/src/androidMain/kotlin/com/arkivanov/sample/shared/root/RootView.kt @@ -14,6 +14,7 @@ import com.arkivanov.sample.shared.root.RootComponent.Child.CountersChild import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.MultiPaneChild +import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild import com.google.android.material.bottomnavigation.BottomNavigationView @ExperimentalDecomposeApi @@ -28,10 +29,12 @@ fun ViewContext.RootView(component: RootComponent): View { val newView: View = when (val child = newStack.active.instance) { is CountersChild -> CountersView(child.component) + is CardsChild, is MultiPaneChild, is DynamicFeaturesChild, - is CustomNavigationChild -> NotImplementedView() + is CustomNavigationChild, + is PagesChild -> NotImplementedView() } if ((oldView != null) && (oldStack != null)) { @@ -74,6 +77,7 @@ fun ViewContext.RootView(component: RootComponent): View { is MultiPaneChild -> R.id.tab_multipane is DynamicFeaturesChild -> R.id.tab_dynamic_features is CustomNavigationChild -> R.id.tab_custom_navigation + is PagesChild -> error("Unsupported tab") } navigationView.setOnNavigationItemSelectedListener(listener) @@ -90,4 +94,5 @@ private val RootComponent.Child.index: Int is MultiPaneChild -> 2 is DynamicFeaturesChild -> 3 is CustomNavigationChild -> 4 + is PagesChild -> 5 } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt new file mode 100644 index 000000000..f0c0d8242 --- /dev/null +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/DefaultPagesComponent.kt @@ -0,0 +1,44 @@ +package com.arkivanov.sample.shared.pages + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.pages.ChildPages +import com.arkivanov.decompose.router.pages.Pages +import com.arkivanov.decompose.router.pages.PagesNavigation +import com.arkivanov.decompose.router.pages.childPages +import com.arkivanov.decompose.router.pages.select +import com.arkivanov.decompose.router.pages.selectNext +import com.arkivanov.decompose.router.pages.selectPrev +import com.arkivanov.decompose.value.Value +import com.arkivanov.sample.shared.customnavigation.DefaultKittenComponent +import com.arkivanov.sample.shared.customnavigation.KittenComponent +import com.arkivanov.sample.shared.customnavigation.KittenComponent.ImageType +import kotlinx.serialization.serializer + +@OptIn(ExperimentalDecomposeApi::class) +class DefaultPagesComponent( + componentContext: ComponentContext, +) : PagesComponent, ComponentContext by componentContext { + + private val nav = PagesNavigation() + + override val pages: Value> = + childPages( + source = nav, + serializer = serializer(), + initialPages = { Pages(items = ImageType.entries, selectedIndex = 0) }, + childFactory = { imageType, ctx -> DefaultKittenComponent(ctx, imageType) }, + ) + + override fun selectPage(index: Int) { + nav.select(index = index) + } + + override fun selectNext() { + nav.selectNext() + } + + override fun selectPrev() { + nav.selectPrev() + } +} diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt new file mode 100644 index 000000000..bb46d02b5 --- /dev/null +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/pages/PagesComponent.kt @@ -0,0 +1,16 @@ +package com.arkivanov.sample.shared.pages + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.pages.ChildPages +import com.arkivanov.decompose.value.Value +import com.arkivanov.sample.shared.customnavigation.KittenComponent + +interface PagesComponent { + + @OptIn(ExperimentalDecomposeApi::class) + val pages: Value> + + fun selectPage(index: Int) + fun selectNext() + fun selectPrev() +} diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt index ac286bcd6..64ce3bb41 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt @@ -14,11 +14,13 @@ import com.arkivanov.sample.shared.customnavigation.DefaultCustomNavigationCompo import com.arkivanov.sample.shared.dynamicfeatures.DefaultDynamicFeaturesComponent import com.arkivanov.sample.shared.dynamicfeatures.dynamicfeature.FeatureInstaller import com.arkivanov.sample.shared.multipane.DefaultMultiPaneComponent +import com.arkivanov.sample.shared.pages.DefaultPagesComponent import com.arkivanov.sample.shared.root.RootComponent.Child import com.arkivanov.sample.shared.root.RootComponent.Child.CountersChild import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.MultiPaneChild +import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild import kotlinx.serialization.Serializable @OptIn(ExperimentalDecomposeApi::class) @@ -57,6 +59,7 @@ class DefaultRootComponent( is Config.MultiPane -> MultiPaneChild(DefaultMultiPaneComponent(componentContext)) is Config.DynamicFeatures -> DynamicFeaturesChild(DefaultDynamicFeaturesComponent(componentContext, featureInstaller)) is Config.CustomNavigation -> CustomNavigationChild(DefaultCustomNavigationComponent(componentContext)) + is Config.Pages -> PagesChild(DefaultPagesComponent(componentContext)) } override fun onCountersTabClicked() { @@ -79,12 +82,17 @@ class DefaultRootComponent( navigation.bringToFront(Config.CustomNavigation) } + override fun onPagesTabClicked() { + navigation.bringToFront(Config.Pages) + } + private companion object { private const val WEB_PATH_COUNTERS = "counters" private const val WEB_PATH_CARDS = "cards" private const val WEB_PATH_MULTI_PANE = "multi-pane" private const val WEB_PATH_DYNAMIC_FEATURES = "dynamic-features" private const val WEB_PATH_CUSTOM_NAVIGATION = "custom-navigation" + private const val WEB_PATH_PAGES = "pages" private fun getInitialStack(webHistoryPaths: List?, deepLink: DeepLink): List = webHistoryPaths @@ -105,6 +113,7 @@ class DefaultRootComponent( Config.MultiPane -> "/$WEB_PATH_MULTI_PANE" Config.DynamicFeatures -> "/$WEB_PATH_DYNAMIC_FEATURES" Config.CustomNavigation -> "/$WEB_PATH_CUSTOM_NAVIGATION" + Config.Pages -> "/$WEB_PATH_PAGES" } private fun getConfigForPath(path: String): Config = @@ -114,6 +123,7 @@ class DefaultRootComponent( WEB_PATH_MULTI_PANE -> Config.MultiPane WEB_PATH_DYNAMIC_FEATURES -> Config.DynamicFeatures WEB_PATH_CUSTOM_NAVIGATION -> Config.CustomNavigation + WEB_PATH_PAGES -> Config.Pages else -> Config.Counters } } @@ -134,6 +144,9 @@ class DefaultRootComponent( @Serializable data object CustomNavigation : Config + + @Serializable + data object Pages : Config } sealed interface DeepLink { diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt index f785767dd..10573f3b3 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/PreviewRootComponent.kt @@ -22,4 +22,5 @@ class PreviewRootComponent : RootComponent { override fun onMultiPaneTabClicked() {} override fun onDynamicFeaturesTabClicked() {} override fun onCustomNavigationTabClicked() {} + override fun onPagesTabClicked() {} } diff --git a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt index 9367224e5..903c5a58d 100644 --- a/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt +++ b/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root/RootComponent.kt @@ -7,6 +7,7 @@ import com.arkivanov.sample.shared.counters.CountersComponent import com.arkivanov.sample.shared.customnavigation.CustomNavigationComponent import com.arkivanov.sample.shared.dynamicfeatures.DynamicFeaturesComponent import com.arkivanov.sample.shared.multipane.MultiPaneComponent +import com.arkivanov.sample.shared.pages.PagesComponent interface RootComponent { @@ -17,6 +18,7 @@ interface RootComponent { fun onMultiPaneTabClicked() fun onDynamicFeaturesTabClicked() fun onCustomNavigationTabClicked() + fun onPagesTabClicked() sealed class Child { class CountersChild(val component: CountersComponent) : Child() @@ -24,5 +26,6 @@ interface RootComponent { class MultiPaneChild(val component: MultiPaneComponent) : Child() class DynamicFeaturesChild(val component: DynamicFeaturesComponent) : Child() class CustomNavigationChild(val component: CustomNavigationComponent) : Child() + class PagesChild(val component: PagesComponent) : Child() } } diff --git a/sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt b/sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt index f69b9306b..eccd98589 100644 --- a/sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt +++ b/sample/shared/shared/src/jsMain/kotlin/com/arkivanov/sample/shared/root/RootContent.kt @@ -10,6 +10,7 @@ import com.arkivanov.sample.shared.root.RootComponent.Child.CountersChild import com.arkivanov.sample.shared.root.RootComponent.Child.CustomNavigationChild import com.arkivanov.sample.shared.root.RootComponent.Child.DynamicFeaturesChild import com.arkivanov.sample.shared.root.RootComponent.Child.MultiPaneChild +import com.arkivanov.sample.shared.root.RootComponent.Child.PagesChild import com.arkivanov.sample.shared.useAsState import mui.material.BottomNavigation import mui.material.BottomNavigationAction @@ -58,6 +59,7 @@ var RootContent: FC> = FC { props -> is MultiPaneChild -> componentContent(component = child.component, content = MultiPaneContent) is DynamicFeaturesChild -> componentContent(component = child.component, content = DynamicFeaturesContent) is CustomNavigationChild -> NotImplementedContent() + is PagesChild -> NotImplementedContent() }.let {} } @@ -75,6 +77,7 @@ var RootContent: FC> = FC { props -> is MultiPaneChild -> TabItem.MULTI_PANE is DynamicFeaturesChild -> TabItem.DYNAMIC_FEATURES is CustomNavigationChild -> TabItem.CUSTOM_NAVIGATION + is PagesChild -> TabItem.PAGES } onChange = { _, newValue -> @@ -84,6 +87,7 @@ var RootContent: FC> = FC { props -> TabItem.MULTI_PANE -> props.component.onMultiPaneTabClicked() TabItem.DYNAMIC_FEATURES -> props.component.onDynamicFeaturesTabClicked() TabItem.CUSTOM_NAVIGATION -> props.component.onCustomNavigationTabClicked() + TabItem.PAGES -> props.component.onPagesTabClicked() } } @@ -96,7 +100,7 @@ var RootContent: FC> = FC { props -> BottomNavigationAction { value = TabItem.CARDS label = ReactNode("Cards") - icon = Icon.create { +"swipe_up" } + icon = Icon.create { +"note_stack" } } BottomNavigationAction { @@ -116,6 +120,12 @@ var RootContent: FC> = FC { props -> label = ReactNode("Custom Navigation") icon = Icon.create { +"location_on" } } + + BottomNavigationAction { + value = TabItem.CUSTOM_NAVIGATION + label = ReactNode("Pages") + icon = Icon.create { +"swipe" } + } } } } @@ -126,4 +136,5 @@ private enum class TabItem { MULTI_PANE, DYNAMIC_FEATURES, CUSTOM_NAVIGATION, + PAGES, }