diff --git a/apps/vbox/client/androidApp/build.gradle.kts b/apps/vbox/client/androidApp/build.gradle.kts
index 5ac77e8..e530b43 100644
--- a/apps/vbox/client/androidApp/build.gradle.kts
+++ b/apps/vbox/client/androidApp/build.gradle.kts
@@ -7,7 +7,7 @@ plugins {
alias(libs.plugins.convention.ktor.client)
}
-val appVersion = "1.26.4"
+val appVersion = "1.26.5"
android {
buildFeatures { buildConfig = true }
diff --git a/libs/ui/src/commonMain/composeResources/drawable/menu.xml b/libs/ui/src/commonMain/composeResources/drawable/menu.xml
new file mode 100644
index 0000000..be344b1
--- /dev/null
+++ b/libs/ui/src/commonMain/composeResources/drawable/menu.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/libs/ui/src/commonMain/composeResources/drawable/menu_open.xml b/libs/ui/src/commonMain/composeResources/drawable/menu_open.xml
new file mode 100644
index 0000000..2ea4f1e
--- /dev/null
+++ b/libs/ui/src/commonMain/composeResources/drawable/menu_open.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/AppScaffold.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/AppScaffold.kt
index d498d6b..41af505 100644
--- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/AppScaffold.kt
+++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/AppScaffold.kt
@@ -1,52 +1,22 @@
package ru.shadowsparky.ui
-import androidx.compose.foundation.background
-import androidx.compose.foundation.focusGroup
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarDefaults
-import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.material3.WideNavigationRail
-import androidx.compose.material3.WideNavigationRailItem
-import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
-import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
-import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.nav.Route
@Immutable
@@ -141,128 +111,3 @@ fun AppScaffold(
}
}
}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
-@Composable
-private fun DynamicNavigationScaffold(
- modifier: Modifier,
- navigationSuiteType: NavigationSuiteType,
- navigationEntries: List>?,
- currentRoute: T?,
- onSelectNavigation: ((T) -> Unit)?,
- scrollBehavior: TopAppBarScrollBehavior?,
- appBar: @Composable (TopAppBarScrollBehavior?) -> Unit,
- snackbarState: SnackbarHostState?,
- content: @Composable (PaddingValues) -> Unit,
-) {
- val entries = navigationEntries.orEmpty()
- val railState = rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
- val isExpanded = railState.targetValue == WideNavigationRailValue.Expanded
- var railHasFocus by remember { mutableStateOf(false) }
- LaunchedEffect(railHasFocus) {
- if (railHasFocus) railState.expand() else railState.collapse()
- }
- NavigationSuiteScaffoldLayout(
- navigationSuiteType = navigationSuiteType,
- navigationSuite = {
- WideNavigationRail(
- state = railState,
- modifier = Modifier.onFocusChanged { railHasFocus = it.hasFocus }.focusGroup()
- ) {
- entries.forEach { entry ->
- WideNavigationRailItem(
- railExpanded = isExpanded,
- selected = currentRoute == entry.route,
- onClick = { onSelectNavigation?.invoke(entry.route) },
- icon = { Icon(entry.painter, entry.title) },
- label = {
- if (isExpanded) Text(entry.title)
- }
- )
- }
- }
- }
- ) {
- Scaffold(
- modifier = if (scrollBehavior != null) {
- modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
- } else {
- modifier
- },
- topBar = { appBar(scrollBehavior) },
- snackbarHost = {
- snackbarState?.let { SnackbarHost(it) }
- },
- content = content
- )
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun BottomBarScaffold(
- modifier: Modifier,
- navigationEntries: List>?,
- currentRoute: T?,
- appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
- content: @Composable (padding: PaddingValues) -> Unit,
- scrollBehavior: TopAppBarScrollBehavior?,
- onSelectNavigation: ((T) -> Unit)?,
- snackbarState: SnackbarHostState?
-) {
- Scaffold(
- bottomBar = { BottomBar(navigationEntries, currentRoute, onSelectNavigation) },
- topBar = { appBar(scrollBehavior) },
- modifier = if (scrollBehavior != null) {
- modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
- } else {
- modifier
- }, snackbarHost = {
- snackbarState?.let { SnackbarHost(it) }
- },
- content = content
- )
-}
-
-@Composable
-private fun BottomBar(
- navigationEntries: List>?,
- currentRoute: T?,
- onSelectNavigation: ((T) -> Unit)?
-) {
- navigationEntries?.let { entries ->
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .background(
- Brush.verticalGradient(
- listOf(Color.Transparent, MaterialTheme.colorScheme.surfaceContainer)
- )
- )
- .padding(8.dp)
- .windowInsetsPadding(WindowInsets.navigationBars)
- ) {
- NavigationBar(
- containerColor = navBarContentColor(),
- modifier = Modifier.clip(RoundedCornerShape(24.dp)).blurEffect()
- ) {
- entries.forEach { entry ->
- NavigationBarItem(
- selected = currentRoute == entry.route,
- onClick = { onSelectNavigation?.invoke(entry.route) },
- icon = { Icon(entry.painter, contentDescription = entry.title) }
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun navBarContentColor(): Color {
- return if (isBlurEnabled()) {
- MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
- } else {
- NavigationBarDefaults.containerColor
- }
-}
diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BottomScaffold.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BottomScaffold.kt
new file mode 100644
index 0000000..df62fa5
--- /dev/null
+++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BottomScaffold.kt
@@ -0,0 +1,98 @@
+package ru.shadowsparky.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.dp
+import ru.shadowsparky.ui.nav.Route
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun BottomBarScaffold(
+ modifier: Modifier,
+ navigationEntries: List>?,
+ currentRoute: T?,
+ appBar: @Composable (scroll: TopAppBarScrollBehavior?) -> Unit,
+ content: @Composable (padding: PaddingValues) -> Unit,
+ scrollBehavior: TopAppBarScrollBehavior?,
+ onSelectNavigation: ((T) -> Unit)?,
+ snackbarState: SnackbarHostState?
+) {
+ Scaffold(
+ bottomBar = { BottomBar(navigationEntries, currentRoute, onSelectNavigation) },
+ topBar = { appBar(scrollBehavior) },
+ modifier = if (scrollBehavior != null) {
+ modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
+ } else {
+ modifier
+ }, snackbarHost = {
+ snackbarState?.let { SnackbarHost(it) }
+ },
+ content = content
+ )
+}
+
+@Composable
+private fun BottomBar(
+ navigationEntries: List>?,
+ currentRoute: T?,
+ onSelectNavigation: ((T) -> Unit)?
+) {
+ navigationEntries?.let { entries ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ Brush.verticalGradient(
+ listOf(Color.Transparent, MaterialTheme.colorScheme.surfaceContainer)
+ )
+ )
+ .padding(8.dp)
+ .windowInsetsPadding(WindowInsets.navigationBars)
+ ) {
+ NavigationBar(
+ containerColor = navBarContentColor(),
+ modifier = Modifier.clip(RoundedCornerShape(24.dp)).blurEffect()
+ ) {
+ entries.forEach { entry ->
+ NavigationBarItem(
+ selected = currentRoute == entry.route,
+ onClick = { onSelectNavigation?.invoke(entry.route) },
+ icon = { Icon(entry.painter, contentDescription = entry.title) }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun navBarContentColor(): Color {
+ return if (isBlurEnabled()) {
+ MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
+ } else {
+ NavigationBarDefaults.containerColor
+ }
+}
diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/DynamicScaffold.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/DynamicScaffold.kt
new file mode 100644
index 0000000..ec1c187
--- /dev/null
+++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/DynamicScaffold.kt
@@ -0,0 +1,93 @@
+package ru.shadowsparky.ui
+
+import androiddev.libs.ui.generated.resources.Res
+import androiddev.libs.ui.generated.resources.menu
+import androiddev.libs.ui.generated.resources.menu_open
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.WideNavigationRail
+import androidx.compose.material3.WideNavigationRailItem
+import androidx.compose.material3.WideNavigationRailState
+import androidx.compose.material3.WideNavigationRailValue
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
+import androidx.compose.material3.rememberWideNavigationRailState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import org.jetbrains.compose.resources.painterResource
+import ru.shadowsparky.ui.nav.Route
+
+internal val LocalWideNavigationRailState = compositionLocalOf { null }
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
+@Composable
+internal fun DynamicNavigationScaffold(
+ modifier: Modifier,
+ navigationSuiteType: NavigationSuiteType,
+ navigationEntries: List>?,
+ currentRoute: T?,
+ onSelectNavigation: ((T) -> Unit)?,
+ scrollBehavior: TopAppBarScrollBehavior?,
+ appBar: @Composable (TopAppBarScrollBehavior?) -> Unit,
+ snackbarState: SnackbarHostState?,
+ content: @Composable (PaddingValues) -> Unit,
+) {
+ val entries = navigationEntries.orEmpty()
+ val railState = LocalWideNavigationRailState.current
+ ?: rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
+ var isExpanded by remember {
+ mutableStateOf(railState.targetValue == WideNavigationRailValue.Expanded)
+ }
+ LaunchedEffect(isExpanded) {
+ if (!isExpanded) railState.collapse() else railState.expand()
+ }
+ NavigationSuiteScaffoldLayout(
+ navigationSuiteType = navigationSuiteType,
+ navigationSuite = {
+ WideNavigationRail(state = railState) {
+ val resId = if (isExpanded) Res.drawable.menu_open else Res.drawable.menu
+ WideNavigationRailItem(
+ false,
+ onClick = { isExpanded = !isExpanded },
+ icon = { Icon(painterResource(resId), null) },
+ label = {},
+ railExpanded = isExpanded
+ )
+ entries.forEach { entry ->
+ WideNavigationRailItem(
+ railExpanded = isExpanded,
+ selected = currentRoute == entry.route,
+ onClick = { onSelectNavigation?.invoke(entry.route) },
+ icon = { Icon(entry.painter, entry.title) },
+ label = { if (isExpanded) Text(entry.title) }
+ )
+ }
+ }
+ }
+ ) {
+ Scaffold(
+ modifier = if (scrollBehavior != null) {
+ modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
+ } else {
+ modifier
+ },
+ topBar = { appBar(scrollBehavior) },
+ snackbarHost = { snackbarState?.let { SnackbarHost(it) } },
+ content = content
+ )
+ }
+}
diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt
index a79ba01..a0ab650 100644
--- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt
+++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/Expressive.kt
@@ -104,21 +104,14 @@ fun ExpressiveLazyColumn(
verticalArrangement = verticalArrangement
) {
scope.sections.forEachIndexed { sectionIndex, section ->
-
if (sectionIndex != 0 && sectionSpacing > 0.dp) {
item(key = "sectionSpacer_$sectionIndex") {
- Spacer(
- Modifier
- .animateItem()
- .height(sectionSpacing)
- )
+ Spacer(Modifier.animateItem().height(sectionSpacing))
}
}
-
section.nodes.forEachIndexed { index, node ->
val topRadius = if (index == 0) roundedMax else roundedMin
val bottomRadius = if (index == section.nodes.lastIndex) roundedMax else roundedMin
-
item(key = node.key, contentType = node.contentType) {
ExpressiveCard(
topRadius = if (reverseLayout) bottomRadius else topRadius,
diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt
index a1f7152..54184ac 100644
--- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt
+++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/nav/Nav.kt
@@ -2,13 +2,17 @@ package ru.shadowsparky.ui.nav
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
+import androidx.compose.material3.WideNavigationRailValue
+import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatableV2
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
+import ru.shadowsparky.ui.LocalWideNavigationRailState
import ru.shadowsparky.ui.theme.getColorScheme
@OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -17,17 +21,20 @@ fun Nav(
rootComponent: RootComponent<*>,
process: @Composable (child: Child, route: Route) -> Unit
) {
- MaterialExpressiveTheme(getColorScheme()) {
- Children(
- stack = rootComponent.valueChildStack,
- animation = predictiveBackAnimation(
- backHandler = rootComponent.backHandler,
- fallbackAnimation = stackAnimation(fade()),
- onBack = rootComponent::onBack,
- selector = { backEvent, _, _ -> androidPredictiveBackAnimatableV2(backEvent) },
- ),
- ) {
- process(it.instance, it.configuration as Route)
+ val railState = rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
+ CompositionLocalProvider(LocalWideNavigationRailState provides railState) {
+ MaterialExpressiveTheme(getColorScheme()) {
+ Children(
+ stack = rootComponent.valueChildStack,
+ animation = predictiveBackAnimation(
+ backHandler = rootComponent.backHandler,
+ fallbackAnimation = stackAnimation(fade()),
+ onBack = rootComponent::onBack,
+ selector = { backEvent, _, _ -> androidPredictiveBackAnimatableV2(backEvent) },
+ ),
+ ) {
+ process(it.instance, it.configuration as Route)
+ }
}
}
}