diff --git a/app/src/main/java/com/lu4p/fokuslauncher/data/local/PreferencesManager.kt b/app/src/main/java/com/lu4p/fokuslauncher/data/local/PreferencesManager.kt index 64c37e5f..dc7fd129 100644 --- a/app/src/main/java/com/lu4p/fokuslauncher/data/local/PreferencesManager.kt +++ b/app/src/main/java/com/lu4p/fokuslauncher/data/local/PreferencesManager.kt @@ -109,6 +109,8 @@ class PreferencesManager @Inject constructor(@param:ApplicationContext private v */ private val DRAWER_SEARCH_AUTO_LAUNCH_KEY = booleanPreferencesKey("drawer_search_auto_launch") + private val DRAWER_SCROLL_TO_TOP_AUTO_KEYBOARD_KEY = + booleanPreferencesKey("drawer_scroll_to_top_auto_keyboard") private val HAS_COMPLETED_ONBOARDING_KEY = booleanPreferencesKey("has_completed_onboarding") private val ONBOARDING_REACHED_SET_DEFAULT_KEY = booleanPreferencesKey("onboarding_reached_set_default") /** @@ -502,6 +504,11 @@ class PreferencesManager @Inject constructor(@param:ApplicationContext private v suspend fun setDrawerSearchAutoLaunch(enabled: Boolean) = setPref(DRAWER_SEARCH_AUTO_LAUNCH_KEY, enabled) + val drawerScrollToTopAutoKeyboardFlow: Flow = + prefFlow(DRAWER_SCROLL_TO_TOP_AUTO_KEYBOARD_KEY, false) + suspend fun setDrawerScrollToTopAutoKeyboard(enabled: Boolean) = + setPref(DRAWER_SCROLL_TO_TOP_AUTO_KEYBOARD_KEY, enabled) + val drawerAppOpenCountsFlow: Flow> = context.fokusLauncherPreferencesDataStore.data.map { prefs -> parseDrawerOpenCounts(prefs[DRAWER_APP_OPEN_COUNTS_KEY] ?: "") diff --git a/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerScreen.kt b/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerScreen.kt index e39a1f7b..4859ecd9 100644 --- a/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerScreen.kt +++ b/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -57,6 +58,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.delay import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -827,35 +829,37 @@ fun AppDrawerContent( BackHandler { closeWithFocusReset() } - LaunchedEffect(useSidebarCategoryDrawer, showSearch) { - val wantKeyboard = - if (useSidebarCategoryDrawer) showSearch else true - if (!wantKeyboard) return@LaunchedEffect - focusRequester.requestFocus() - keyboardController?.show() + val isAtTop by remember(listState) { + derivedStateOf { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 } } + var hasScrolledDown by remember { mutableStateOf(false) } - LaunchedEffect(listState, keyboardController, focusManager) { - var prevIndex = listState.firstVisibleItemIndex - var prevOffset = listState.firstVisibleItemScrollOffset - snapshotFlow { - Triple( - listState.isScrollInProgress, - listState.firstVisibleItemIndex, - listState.firstVisibleItemScrollOffset - ) - }.collect { (scrolling, index, offset) -> - if (scrolling) { - val scrolledDown = - index > prevIndex || - (index == prevIndex && offset > prevOffset) - if (scrolledDown) { - keyboardController?.hide() - focusManager.clearFocus(force = true) - } + // Unified Search/Keyboard management: Handles entry, scroll-to-top, and pull-to-open + LaunchedEffect(isAtTop, showSearch, useSidebarCategoryDrawer, uiState.drawerScrollToTopAutoKeyboard) { + // Trigger keyboard if: + // 1. Initial entry or setting is on, AND we are at the top + // 2. Search is explicitly toggled on in sidebar mode + val topAutoLaunch = isAtTop && (!hasScrolledDown || uiState.drawerScrollToTopAutoKeyboard) + + if (topAutoLaunch) { + if (useSidebarCategoryDrawer && !showSearch) { + showSearch = true + } else { + delay(100) + focusRequester.requestFocus() + keyboardController?.show() } - prevIndex = index - prevOffset = offset + } else if (useSidebarCategoryDrawer && showSearch) { + // Focus if search was explicitly toggled + delay(100) + focusRequester.requestFocus() + keyboardController?.show() + } else if (!isAtTop) { + // Track that we've left the top once + hasScrolledDown = true + keyboardController?.hide() + focusManager.clearFocus(force = true) + if (useSidebarCategoryDrawer) showSearch = false } } @@ -864,6 +868,15 @@ fun AppDrawerContent( object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (source == NestedScrollSource.UserInput && available.y > 0 && !listState.canScrollBackward) { + // Immediate response for pull-down gesture at the boundary + if (uiState.drawerScrollToTopAutoKeyboard) { + // The LaunchedEffect(isAtTop) handles the actual focus/keyboard + // but if we are already atTop, it won't re-trigger. + // So we force a request here if already at top and pulling. + if (useSidebarCategoryDrawer && !showSearch) showSearch = true + focusRequester.requestFocus() + keyboardController?.show() + } overscrollY += available.y if (overscrollY > 300f) { overscrollY = 0f diff --git a/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerViewModel.kt b/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerViewModel.kt index 13c80223..50a2b9da 100644 --- a/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerViewModel.kt +++ b/app/src/main/java/com/lu4p/fokuslauncher/ui/drawer/AppDrawerViewModel.kt @@ -74,6 +74,8 @@ data class AppDrawerUiState( val categoryDrawerIconOverrides: Map = emptyMap(), val usesPhotoWallpaper: Boolean = false, val drawerAppSortMode: DrawerAppSortMode = DrawerAppSortMode.ALPHABETICAL, + /** Automatically open keyboard when scrolling to the top of the app drawer. */ + val drawerScrollToTopAutoKeyboard: Boolean = false, /** * When true with [drawerAppSortMode] CUSTOM and sidebar layout, the list shows drag handles and * can be reordered. Cleared when the drawer closes, search filters, or CUSTOM layout is unavailable. @@ -277,6 +279,7 @@ constructor( observeDrawerCategoryRailAndIcons() observeDrawerSortOpenCountsAndCustomOrder() observeDrawerDotSearchPreferences() + observeDrawerScrollToTopAutoKeyboard() observeLauncherAppearance() observeDrawerSearchAutoLaunch() refreshPrivateSpaceState() @@ -312,6 +315,14 @@ constructor( } } + private fun observeDrawerScrollToTopAutoKeyboard() { + viewModelScope.launch { + preferencesManager.drawerScrollToTopAutoKeyboardFlow.collect { enabled -> + _uiState.update { it.copy(drawerScrollToTopAutoKeyboard = enabled) } + } + } + } + /** * Loads profile-section and private-app sort caches for the current [AppDrawerUiState] without * publishing UI. Schedules work on [viewModelScope] so the drawer’s first open avoids cold CPU diff --git a/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsScreen.kt b/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsScreen.kt index 0f0df778..892b46aa 100644 --- a/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsScreen.kt @@ -540,6 +540,14 @@ private fun SettingsScreenContent( onCheckedChange = viewModel::setDrawerSearchAutoLaunch ) } + item { + SettingsToggleRow( + label = stringResource(R.string.settings_drawer_scroll_to_top_auto_keyboard), + subtitle = stringResource(R.string.settings_drawer_scroll_to_top_auto_keyboard_subtitle), + checked = uiState.drawerScrollToTopAutoKeyboard, + onCheckedChange = viewModel::setDrawerScrollToTopAutoKeyboard + ) + } item { SettingsToggleRow( label = stringResource(R.string.settings_drawer_sidebar_categories), diff --git a/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsViewModel.kt index 9d44e758..cf6b4981 100644 --- a/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/lu4p/fokuslauncher/ui/settings/SettingsViewModel.kt @@ -79,6 +79,8 @@ data class SettingsUiState( val drawerSidebarCategories: Boolean = false, /** Launch the app when search narrows to a single match (drawer search). */ val drawerSearchAutoLaunch: Boolean = true, + /** Auto-launch keyboard when scrolling to the top of the app drawer. */ + val drawerScrollToTopAutoKeyboard: Boolean = false, /** When true, category rail is on the left; default false places it on the right. */ val drawerCategorySidebarOnLeft: Boolean = false, /** Normalized category key → [MinimalIcons] name for the drawer sidebar rail. */ @@ -226,17 +228,23 @@ constructor( preferencesManager.drawerSidebarCategoriesFlow, preferencesManager.drawerAppSortModeFlow, preferencesManager.drawerSearchAutoLaunchFlow, - ) { sidebarCategories, sortMode, searchAutoLaunch -> - Triple(sidebarCategories, sortMode, searchAutoLaunch) + preferencesManager.drawerScrollToTopAutoKeyboardFlow, + ) { sidebarCategories, sortMode, searchAutoLaunch, scrollToTopAutoKeyboard -> + DrawerPrefs( + swipeRightTarget = null, // placeholder + preferredWeatherAppPackage = "", // placeholder + showStatusBar = false, // placeholder + drawerSidebarCategories = sidebarCategories, + drawerAppSortMode = sortMode, + drawerSearchAutoLaunch = searchAutoLaunch, + drawerScrollToTopAutoKeyboard = scrollToTopAutoKeyboard, + ) }, ) { swipeAndWeather, drawerLayout -> - DrawerPrefs( + drawerLayout.copy( swipeRightTarget = swipeAndWeather.first, preferredWeatherAppPackage = swipeAndWeather.second, showStatusBar = swipeAndWeather.third, - drawerSidebarCategories = drawerLayout.first, - drawerAppSortMode = drawerLayout.second, - drawerSearchAutoLaunch = drawerLayout.third, ) } val fontVisualFlow = @@ -365,6 +373,7 @@ constructor( temperatureUnit = homeWidgetItems.temperatureUnit, drawerSidebarCategories = drawer.drawerSidebarCategories, drawerSearchAutoLaunch = drawer.drawerSearchAutoLaunch, + drawerScrollToTopAutoKeyboard = drawer.drawerScrollToTopAutoKeyboard, drawerCategorySidebarOnLeft = lockRail.drawerCategorySidebarOnLeft, categoryDrawerIconOverrides = lockRail.categoryDrawerIconOverrides, drawerAppSortMode = drawer.drawerAppSortMode, @@ -423,6 +432,7 @@ constructor( val drawerSidebarCategories: Boolean, val drawerAppSortMode: DrawerAppSortMode, val drawerSearchAutoLaunch: Boolean, + val drawerScrollToTopAutoKeyboard: Boolean, ) private data class FontVisualPrefs( @@ -662,6 +672,9 @@ constructor( fun setDrawerSearchAutoLaunch(enabled: Boolean) = launchPreferences { setDrawerSearchAutoLaunch(enabled) } + fun setDrawerScrollToTopAutoKeyboard(enabled: Boolean) = + launchPreferences { setDrawerScrollToTopAutoKeyboard(enabled) } + fun setDrawerCategorySidebarOnLeft(onLeft: Boolean) = launchPreferences { setDrawerCategorySidebarOnLeft(onLeft) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f10eb584..3f1dbbac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -349,4 +349,8 @@ Contribute translations on Weblate Join the discussion Get help and share feedback + + + Keyboard on scroll to top + Automatically open keyboard and search when scrolling to the top of the app list