diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 59dec480..778259cc 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -387,6 +387,10 @@
Follow system
Hide activity section
Hides the activity section in the recipe details view
+ Hides the bottom navigation bar when scrolling down
+ Hide bottom bar on scroll
+ Keeps the search bar at the top on the home tab
+ Pin home search bar
Appearance
Change kitshn's behavior
Enable crash reporting
diff --git a/composeApp/src/commonMain/kotlin/de/kitshn/Settings.kt b/composeApp/src/commonMain/kotlin/de/kitshn/Settings.kt
index 73911fc2..afa04f80 100644
--- a/composeApp/src/commonMain/kotlin/de/kitshn/Settings.kt
+++ b/composeApp/src/commonMain/kotlin/de/kitshn/Settings.kt
@@ -26,6 +26,8 @@ const val KEY_SETTINGS_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme"
const val KEY_SETTINGS_APPEARANCE_CUSTOM_COLOR_SCHEME_SEED = "appearance_custom_color_scheme_seed"
const val KEY_SETTINGS_APPEARANCE_ENLARGE_SHOPPING_MODE = "appearance_enlarge_shopping_mode"
const val KEY_SETTINGS_APPEARANCE_HIDE_ACTIVITY = "appearance_hide_activity"
+const val KEY_SETTINGS_APPEARANCE_HIDE_BOTTOM_BAR_ON_SCROLL = "appearance_hide_bottom_bar_on_scroll"
+const val KEY_SETTINGS_APPEARANCE_PIN_HOME_SEARCH_BAR = "appearance_pin_home_search_bar"
const val KEY_SETTINGS_BEHAVIOR_USE_SHARE_WRAPPER = "behavior_use_share_wrapper"
const val KEY_SETTINGS_BEHAVIOR_USE_SHARE_WRAPPER_HINT_SHOWN =
@@ -152,6 +154,18 @@ class SettingsViewModel : ViewModel() {
fun setHideActivity(hide: Boolean) =
obs.putBoolean(KEY_SETTINGS_APPEARANCE_HIDE_ACTIVITY, hide)
+ val getHideBottomBarOnScroll: Flow =
+ obs.getBooleanFlow(KEY_SETTINGS_APPEARANCE_HIDE_BOTTOM_BAR_ON_SCROLL, false)
+
+ fun setHideBottomBarOnScroll(hide: Boolean) =
+ obs.putBoolean(KEY_SETTINGS_APPEARANCE_HIDE_BOTTOM_BAR_ON_SCROLL, hide)
+
+ val getPinHomeSearchBar: Flow =
+ obs.getBooleanFlow(KEY_SETTINGS_APPEARANCE_PIN_HOME_SEARCH_BAR, false)
+
+ fun setPinHomeSearchBar(pin: Boolean) =
+ obs.putBoolean(KEY_SETTINGS_APPEARANCE_PIN_HOME_SEARCH_BAR, pin)
+
// behavior
val getUseShareWrapper: Flow =
obs.getBooleanFlow(KEY_SETTINGS_BEHAVIOR_USE_SHARE_WRAPPER, true)
diff --git a/composeApp/src/commonMain/kotlin/de/kitshn/ui/component/ScrollToHideConnection.kt b/composeApp/src/commonMain/kotlin/de/kitshn/ui/component/ScrollToHideConnection.kt
new file mode 100644
index 00000000..6fdb1ae0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/de/kitshn/ui/component/ScrollToHideConnection.kt
@@ -0,0 +1,92 @@
+package de.kitshn.ui.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.unit.Velocity
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Composable
+fun rememberScrollToHideConnection(
+ enabled: Boolean,
+ onHide: suspend () -> Unit,
+ onShow: suspend () -> Unit
+): NestedScrollConnection {
+ val scope = rememberCoroutineScope()
+
+ return remember(enabled) {
+ var showJob: Job? = null
+ var scrollDelta = 0f
+
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ if (!enabled) return super.onPreScroll(available, source)
+
+ val delta = available.y
+
+ // Reset accumulation on direction change
+ if ((delta > 0 && scrollDelta < 0) || (delta < 0 && scrollDelta > 0)) {
+ scrollDelta = 0f
+ }
+
+ // Restart show job on any interaction
+ if (source == NestedScrollSource.UserInput) {
+ showJob?.cancel()
+ showJob = scope.launch {
+ delay(600)
+ onShow()
+ scrollDelta = 0f
+ }
+ }
+
+ return super.onPreScroll(available, source)
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ if (!enabled) return super.onPostScroll(consumed, available, source)
+
+ // Only accumulate downward scroll if content actually moved
+ // (prevents hiding at the bottom of a list or when not scrollable)
+ if (consumed.y < 0) {
+ scrollDelta += consumed.y
+ }
+
+ // Accumulate upward scroll (consumed OR available/overscroll at the top)
+ // ensures the bar shows even when the content is already at the top.
+ if (consumed.y > 0 || available.y > 0) {
+ scrollDelta += (consumed.y + available.y)
+ }
+
+ if (scrollDelta < -20f) { // Significant scroll down
+ showJob?.cancel()
+ scope.launch { onHide() }
+ scrollDelta = 0f
+ } else if (scrollDelta > 20f) { // Significant scroll up
+ showJob?.cancel()
+ scope.launch { onShow() }
+ scrollDelta = 0f
+ }
+
+ return super.onPostScroll(consumed, available, source)
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ if (enabled) {
+ showJob?.cancel()
+ onShow()
+ scrollDelta = 0f
+ }
+ return super.onPostFling(consumed, available)
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/Main.kt b/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/Main.kt
index 66c6a006..4c16b553 100644
--- a/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/Main.kt
+++ b/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/Main.kt
@@ -14,19 +14,25 @@ import androidx.compose.material.icons.rounded.ShoppingCart
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldValue
+import androidx.compose.material3.adaptive.navigationsuite.rememberNavigationSuiteScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextOverflow
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import de.kitshn.saveBreadcrumb
+import de.kitshn.ui.component.rememberScrollToHideConnection
import de.kitshn.ui.dialog.version.TandoorBetaInfoDialog
import de.kitshn.ui.dialog.version.TandoorServerVersionCompatibilityDialog
import de.kitshn.ui.route.RouteParameters
@@ -37,6 +43,7 @@ import kitshn.composeapp.generated.resources.navigation_home
import kitshn.composeapp.generated.resources.navigation_meal_plan
import kitshn.composeapp.generated.resources.navigation_settings
import kitshn.composeapp.generated.resources.navigation_shopping
+import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
@@ -79,6 +86,18 @@ fun RouteMain(p: RouteParameters) {
var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) }
val mainSubNavHostController = rememberAlternateNavController()
+ val scaffoldState = rememberNavigationSuiteScaffoldState()
+ val scope = rememberCoroutineScope()
+
+ val hideBottomBarOnScroll =
+ p.vm.settings.getHideBottomBarOnScroll.collectAsState(initial = false)
+
+ val nestedScrollConnection = rememberScrollToHideConnection(
+ enabled = hideBottomBarOnScroll.value,
+ onHide = { scaffoldState.hide() },
+ onShow = { scaffoldState.show() }
+ )
+
val destination by mainSubNavHostController.currentBackStackEntryAsState()
LaunchedEffect(destination) {
val route = destination?.destination?.route ?: ""
@@ -88,12 +107,19 @@ fun RouteMain(p: RouteParameters) {
if(!route.startsWith(it.route)) return@forEach
currentDestination = it
}
+
+ // Show bottom bar when destination changes
+ if (scaffoldState.currentValue == NavigationSuiteScaffoldValue.Hidden) {
+ scope.launch { scaffoldState.show() }
+ }
}
val isOffline = p.vm.uiState.offlineState.isOffline
NavigationSuiteScaffold(
- navigationSuiteColors = NavigationSuiteDefaults.colors(
+ state = scaffoldState,
+ modifier = Modifier.nestedScroll(nestedScrollConnection),
+ navigationSuiteColors = androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults.colors(
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest
),
navigationSuiteItems = {
diff --git a/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/Home.kt b/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/Home.kt
index c75d6317..01144c3a 100644
--- a/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/Home.kt
+++ b/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/Home.kt
@@ -113,7 +113,11 @@ fun RouteMainSubrouteHome(
val selectionModeState = rememberSelectionModeState()
- val searchBarScrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior()
+ val pinHomeSearchBar by p.vm.settings.getPinHomeSearchBar.collectAsState(initial = false)
+
+ val searchBarScrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior(
+ canScroll = { !pinHomeSearchBar }
+ )
var isScrollingUp by rememberSaveable { mutableStateOf(true) }
diff --git a/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/HomeDynamicLayout.kt b/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/HomeDynamicLayout.kt
index e9a632c8..e040f52d 100644
--- a/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/HomeDynamicLayout.kt
+++ b/composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/subroute/home/HomeDynamicLayout.kt
@@ -195,7 +195,7 @@ fun HomeDynamicLayout(
}
}
- if(homePageSectionList.size == 0 && pageLoadingState != ErrorLoadingSuccessState.SUCCESS) {
+ if(homePageSectionList.isEmpty() && pageLoadingState != ErrorLoadingSuccessState.SUCCESS) {
repeat(5) {
HomePageSectionView(
client = p.vm.tandoorClient,
diff --git a/composeApp/src/commonMain/kotlin/de/kitshn/ui/view/settings/SettingsAppearance.kt b/composeApp/src/commonMain/kotlin/de/kitshn/ui/view/settings/SettingsAppearance.kt
index a1d4985b..5bb8a4b8 100644
--- a/composeApp/src/commonMain/kotlin/de/kitshn/ui/view/settings/SettingsAppearance.kt
+++ b/composeApp/src/commonMain/kotlin/de/kitshn/ui/view/settings/SettingsAppearance.kt
@@ -3,6 +3,7 @@ package de.kitshn.ui.view.settings
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ViewList
import androidx.compose.material.icons.rounded.AutoAwesome
import androidx.compose.material.icons.rounded.CommentsDisabled
import androidx.compose.material.icons.rounded.DarkMode
@@ -46,6 +47,10 @@ import kitshn.composeapp.generated.resources.settings_section_appearance_follow_
import kitshn.composeapp.generated.resources.settings_section_appearance_follow_system_label
import kitshn.composeapp.generated.resources.settings_section_appearance_hide_activity_description
import kitshn.composeapp.generated.resources.settings_section_appearance_hide_activity_label
+import kitshn.composeapp.generated.resources.settings_section_appearance_hide_bottom_bar_on_scroll_description
+import kitshn.composeapp.generated.resources.settings_section_appearance_hide_bottom_bar_on_scroll_label
+import kitshn.composeapp.generated.resources.settings_section_appearance_pin_home_search_bar_description
+import kitshn.composeapp.generated.resources.settings_section_appearance_pin_home_search_bar_label
import kitshn.composeapp.generated.resources.settings_section_appearance_label
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@@ -175,6 +180,40 @@ fun ViewSettingsAppearance(
}
}
}
+
+ item {
+ val hideBottomBarOnScroll = p.vm.settings.getHideBottomBarOnScroll.collectAsState(initial = false)
+
+ SettingsSwitchListItem(
+ position = SettingsListItemPosition.TOP,
+ label = { Text(stringResource(Res.string.settings_section_appearance_hide_bottom_bar_on_scroll_label)) },
+ description = { Text(stringResource(Res.string.settings_section_appearance_hide_bottom_bar_on_scroll_description)) },
+ icon = Icons.AutoMirrored.Rounded.ViewList,
+ contentDescription = stringResource(Res.string.settings_section_appearance_hide_bottom_bar_on_scroll_label),
+ checked = hideBottomBarOnScroll.value
+ ) {
+ coroutineScope.launch {
+ p.vm.settings.setHideBottomBarOnScroll(it)
+ }
+ }
+ }
+
+ item {
+ val pinHomeSearchBar = p.vm.settings.getPinHomeSearchBar.collectAsState(initial = false)
+
+ SettingsSwitchListItem(
+ position = SettingsListItemPosition.BOTTOM,
+ label = { Text(stringResource(Res.string.settings_section_appearance_pin_home_search_bar_label)) },
+ description = { Text(stringResource(Res.string.settings_section_appearance_pin_home_search_bar_description)) },
+ icon = Icons.Rounded.Loupe,
+ contentDescription = stringResource(Res.string.settings_section_appearance_pin_home_search_bar_label),
+ checked = pinHomeSearchBar.value
+ ) {
+ coroutineScope.launch {
+ p.vm.settings.setPinHomeSearchBar(it)
+ }
+ }
+ }
}
}