Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@
<string name="settings_section_appearance_follow_system_label">Follow system</string>
<string name="settings_section_appearance_hide_activity_label">Hide activity section</string>
<string name="settings_section_appearance_hide_activity_description">Hides the activity section in the recipe details view</string>
<string name="settings_section_appearance_hide_bottom_bar_on_scroll_description">Hides the bottom navigation bar when scrolling down</string>
<string name="settings_section_appearance_hide_bottom_bar_on_scroll_label">Hide bottom bar on scroll</string>
<string name="settings_section_appearance_pin_home_search_bar_description">Keeps the search bar at the top on the home tab</string>
<string name="settings_section_appearance_pin_home_search_bar_label">Pin home search bar</string>
<string name="settings_section_appearance_label">Appearance</string>
<string name="settings_section_behavior_description">Change kitshn's behavior</string>
<string name="settings_section_behavior_enable_crash_reporting">Enable crash reporting</string>
Expand Down
14 changes: 14 additions & 0 deletions composeApp/src/commonMain/kotlin/de/kitshn/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -152,6 +154,18 @@ class SettingsViewModel : ViewModel() {
fun setHideActivity(hide: Boolean) =
obs.putBoolean(KEY_SETTINGS_APPEARANCE_HIDE_ACTIVITY, hide)

val getHideBottomBarOnScroll: Flow<Boolean> =
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<Boolean> =
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<Boolean> =
obs.getBooleanFlow(KEY_SETTINGS_BEHAVIOR_USE_SHARE_WRAPPER, true)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
30 changes: 28 additions & 2 deletions composeApp/src/commonMain/kotlin/de/kitshn/ui/route/main/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 ?: ""
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ fun RouteMainSubrouteHome(

val selectionModeState = rememberSelectionModeState<Int>()

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) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
}

Expand Down