Rewrite of the underlying layout and gesture handling
This commit is contained in:
Valerie 2025-05-27 00:02:12 +02:00 committed by GitHub
parent fe322fc558
commit a24d1b8798
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 3889 additions and 2593 deletions

View File

@ -34,6 +34,7 @@
<option name="STATIC_FIELD_NAME_PREFIX" value="s" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />

View File

@ -51,6 +51,7 @@ android {
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
"-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi",
"-Xwhen-guards",
)
}
@ -95,6 +96,8 @@ dependencies {
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.navigationanimation)
implementation(libs.haze)
implementation(libs.androidx.core)
implementation(libs.androidx.activitycompose)
implementation(libs.bundles.androidx.lifecycle)

View File

@ -14,6 +14,7 @@
android:resumeWhilePausing="true"
android:stateNotNeeded="true"
android:theme="@style/LauncherTheme"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -37,11 +38,13 @@
android:name=".assistant.AssistantActivity"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:taskAffinity="de.mm20.launcher2.assistant"
android:resumeWhilePausing="true"
android:stateNotNeeded="true"
android:theme="@style/LauncherTheme"
android:windowSoftInputMode="stateHidden|adjustResize">
android:theme="@style/AssistantTheme"
android:windowSoftInputMode="stateHidden|adjustResize"
android:enableOnBackInvokedCallback="true">
<intent-filter>
<action android:name="android.intent.action.ASSIST" />
<category android:name="android.intent.category.DEFAULT" />

View File

@ -1,220 +0,0 @@
package de.mm20.launcher2.ui.assistant
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.*
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
import de.mm20.launcher2.ui.launcher.gestures.LauncherGestureHandler
import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@Composable
fun AssistantScaffold(
modifier: Modifier = Modifier,
darkStatusBarIcons: Boolean = false,
darkNavBarIcons: Boolean = false,
bottomSearchBar: Boolean = false,
reverseSearchResults: Boolean = false,
fixedSearchBar: Boolean = false,
) {
val viewModel: LauncherScaffoldVM = viewModel()
val searchVM: SearchVM = viewModel()
val context = LocalContext.current
var searchBarFocused by remember { mutableStateOf(false) }
val lifecycleOwner = LocalLifecycleOwner.current
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(null) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
searchBarFocused = false
if (!viewModel.autoFocusSearch.first()) return@repeatOnLifecycle
delay(100)
searchBarFocused = true
keyboardController?.show()
}
}
val searchState = rememberLazyListState()
val filterBar by searchVM.filterBar.collectAsState(false)
val keyboardFilterBarPadding by animateDpAsState(
if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value && filterBar) 50.dp else 0.dp
)
val isSearchAtStart by remember {
derivedStateOf {
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
}
}
val isSearchAtEnd by remember {
derivedStateOf {
val lastItem =
searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
}
}
val searchBarLevel by remember {
derivedStateOf {
when {
reverseSearchResults == bottomSearchBar && isSearchAtStart -> SearchBarLevel.Active
reverseSearchResults != bottomSearchBar && isSearchAtEnd -> SearchBarLevel.Active
else -> SearchBarLevel.Raised
}
}
}
val systemUiController = rememberSystemUiController()
val showStatusBarScrim by remember {
derivedStateOf {
if (reverseSearchResults) {
!isSearchAtEnd
} else {
!isSearchAtStart
}
}
}
val showNavBarScrim by remember {
derivedStateOf {
if (reverseSearchResults) {
!isSearchAtStart
} else {
!isSearchAtEnd
}
}
}
val colorSurface = MaterialTheme.colorScheme.surface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(darkStatusBarIcons, colorSurface, showStatusBarScrim) {
if (showStatusBarScrim) {
systemUiController.setStatusBarColor(
colorSurface.copy(0.7f),
)
} else {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = !isDarkTheme,
)
}
}
LaunchedEffect(darkNavBarIcons, showNavBarScrim) {
if (showNavBarScrim) {
systemUiController.setNavigationBarColor(
colorSurface.copy(0.7f),
)
} else {
systemUiController.setNavigationBarColor(
Color.Transparent,
darkIcons = darkNavBarIcons,
navigationBarContrastEnforced = false
)
}
}
val density = LocalDensity.current
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
var searchBarOffset by remember {
mutableFloatStateOf(0f)
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val y = available.y * if (reverseSearchResults) -1f else 1f
searchBarOffset = (searchBarOffset + y).coerceIn(-maxSearchBarOffset, 0f)
return super.onPreScroll(available, source)
}
}
}
val actions = searchVM.searchActionResults
val webSearchPadding by animateDpAsState(
if (actions.isEmpty()) 0.dp else 48.dp
)
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
val cardStyle = LocalCardStyle.current
Box(
modifier = modifier
.fillMaxSize()
.background(
MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.85f * cardStyle.opacity)
)
.nestedScroll(nestedScrollConnection)
) {
SearchColumn(
modifier = Modifier.fillMaxSize(),
paddingValues = PaddingValues(
top = (if (bottomSearchBar) 0.dp else 64.dp + webSearchPadding) + 8.dp + windowInsets.calculateTopPadding(),
bottom = (if (bottomSearchBar) 64.dp + webSearchPadding else 0.dp) + 8.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
),
reverse = reverseSearchResults,
state = searchState
)
val value by searchVM.searchQuery
val searchBarColor by viewModel.searchBarColor.collectAsState()
val searchBarStyle by viewModel.searchBarStyle.collectAsState()
val launchOnEnter by searchVM.launchOnEnter.collectAsState(false)
LauncherSearchBar(
modifier = Modifier
.fillMaxSize(),
style = searchBarStyle,
level = { searchBarLevel },
value = { value },
focused = searchBarFocused,
onFocusChange = {
if (it) viewModel.openSearch()
viewModel.setSearchbarFocus(it)
},
actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction,
isSearchOpen = true,
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
bottomSearchBar = bottomSearchBar,
searchBarOffset = {
(if (searchBarFocused || fixedSearchBar) 0
else searchBarOffset.toInt() * if (bottomSearchBar) -1 else 1)
- (if (bottomSearchBar) with(density) { keyboardFilterBarPadding.toPx() }.toInt() else 0)
},
onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) }
} else null
)
}
LauncherGestureHandler()
}

View File

@ -9,6 +9,8 @@ import de.mm20.launcher2.ui.component.ProvideIconShape
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
import de.mm20.launcher2.ui.theme.transparency.TransparencyScheme
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.combine
@ -46,6 +48,10 @@ fun ProvideSettings(
LocalCardStyle provides cardStyle,
LocalFavoritesEnabled provides favoritesEnabled,
LocalGridSettings provides gridSettings,
LocalTransparencyScheme provides TransparencyScheme(
background = cardStyle.opacity * 0.85f,
surface = cardStyle.opacity,
)
) {
ProvideIconShape(iconShape) {
content()

View File

@ -73,7 +73,6 @@ fun FavoritesTagSelector(
Row(
modifier = Modifier
.weight(1f)
.consumeAllScrolling()
.horizontalScroll(scrollState)
.padding(end = 12.dp),
) {

View File

@ -51,7 +51,6 @@ fun FakeSplashScreen(
Surface(
modifier = modifier
.fillMaxSize(),
shadowElevation = 4.dp,
color = animatedBackgroundColor,
) {
Box(

View File

@ -25,12 +25,13 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
@Composable
fun LauncherCard(
modifier: Modifier = Modifier,
elevation: Dp = 2.dp,
backgroundOpacity: Float = LocalCardStyle.current.opacity,
backgroundOpacity: Float = LocalTransparencyScheme.current.surface,
shape: Shape = MaterialTheme.shapes.medium,
color: Color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
border: BorderStroke? = LocalCardStyle.current.borderWidth.takeIf { it > 0 }
@ -55,7 +56,7 @@ fun PartialLauncherCard(
isTop: Boolean = false,
isBottom: Boolean = false,
elevation: Dp = 2.dp,
backgroundOpacity: Float = LocalCardStyle.current.opacity,
backgroundOpacity: Float = LocalTransparencyScheme.current.surface,
content: @Composable () -> Unit
) {
@ -79,7 +80,7 @@ fun PartialLauncherCard(
private fun CardMiddlePiece(
modifier: Modifier,
elevation: Dp,
backgroundOpacity: Float = LocalCardStyle.current.opacity,
backgroundOpacity: Float = LocalTransparencyScheme.current.surface,
content: @Composable () -> Unit
) {
val borderWidth = LocalCardStyle.current.borderWidth.dp

View File

@ -47,6 +47,7 @@ import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
@Composable
fun SearchBar(
@ -102,7 +103,7 @@ fun SearchBar(
}
}) {
when {
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
it == SearchBarLevel.Active -> LocalTransparencyScheme.current.surface
style != SearchBarStyle.Transparent -> 1f
it == SearchBarLevel.Resting -> 0f
else -> 1f
@ -165,9 +166,6 @@ fun SearchBar(
color = contentColor
)
}
LaunchedEffect(level) {
if (level == SearchBarLevel.Resting) onUnfocus()
}
BasicTextField(
modifier = Modifier
.onFocusChanged {
@ -207,7 +205,7 @@ fun SearchBar(
}
}
enum class SearchBarLevel {
enum class SearchBarLevel: Comparable<SearchBarLevel> {
/**
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
* and search is closed)

View File

@ -1,10 +0,0 @@
package de.mm20.launcher2.ui.gestures
enum class Gesture {
DoubleTap,
LongPress,
SwipeDown,
SwipeLeft,
SwipeRight,
HomeButton,
}

View File

@ -1,67 +0,0 @@
package de.mm20.launcher2.ui.gestures
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.geometry.Offset
class GestureDetector {
private var dragStart: Offset? = null
var currentDrag : Offset? = null
var gestureListener: OnGestureListener? = null
var shouldDetectDoubleTaps by mutableStateOf(false)
fun dispatchTap(position: Offset) {
gestureListener?.onTap(position)
}
fun dispatchDoubleTap(position: Offset) {
gestureListener?.onDoubleTap(position)
}
fun dispatchLongPress(position: Offset) {
gestureListener?.onLongPress(position)
}
private var hasDragEnded = false
fun dispatchDrag(offset: Offset) {
if (hasDragEnded) return
val totalDrag = currentDrag?.plus(offset) ?: offset
currentDrag = totalDrag
if (gestureListener?.onDrag(totalDrag) == true) hasDragEnded = true
}
fun dispatchDragEnd() {
dragStart = null
currentDrag = null
hasDragEnded = false
gestureListener?.onDragEnd()
}
fun dispatchHomeButtonPress() {
gestureListener?.onHomeButtonPress()
}
interface OnGestureListener {
fun onTap(position: Offset) {}
fun onDoubleTap(position: Offset) {}
fun onLongPress(position: Offset) {}
/**
* @return true if the drag gesture has been handled.
* The gesture detector will no longer track the drag gesture in this case.
*/
fun onDrag(offset: Offset): Boolean = false
fun onDragEnd() {}
fun onHomeButtonPress() {}
}
}
val LocalGestureDetector = staticCompositionLocalOf<GestureDetector> {
GestureDetector()
}

View File

@ -1,47 +0,0 @@
package de.mm20.launcher2.ui.gestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.geometry.Offset
@Composable
fun GestureHandler(
detector: GestureDetector,
onTap: (Offset) -> Unit = {},
onLongPress: (Offset) -> Unit = {},
onDoubleTap: (Offset) -> Unit = {},
onDrag: (Offset) -> Boolean = { false },
onDragEnd: () -> Unit = {},
onHomeButtonPress: () -> Unit = {},
) {
DisposableEffect(detector) {
detector.gestureListener = object : GestureDetector.OnGestureListener {
override fun onTap(position: Offset) {
onTap(position)
}
override fun onLongPress(position: Offset) {
onLongPress(position)
}
override fun onDoubleTap(position: Offset) {
onDoubleTap(position)
}
override fun onDrag(offset: Offset): Boolean {
return onDrag(offset)
}
override fun onDragEnd() {
onDragEnd()
}
override fun onHomeButtonPress() {
onHomeButtonPress()
}
}
onDispose {
detector.gestureListener = null
}
}
}

View File

@ -7,29 +7,14 @@ import com.android.launcher3.GestureNavContract
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val navContract = intent?.let { GestureNavContract.fromIntent(it) }
val navContract = intent.let { GestureNavContract.fromIntent(it) }
if (navContract != null) {
enterHomeTransitionManager.resolve(navContract, window)
} else if (System.currentTimeMillis() - pausedAt < 50) {
// If the onPause was called less than 50ms ago, we assume that the app was already
// in the foreground when the user pressed the home button. In this case, we dispatch
// the home button press event to the gesture detector.
gestureDetector.dispatchHomeButtonPress()
}
}
private var pausedAt: Long = 0
override fun onPause() {
super.onPause()
enterHomeTransitionManager.clear()
pausedAt = System.currentTimeMillis()
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (onBackPressedDispatcher.hasEnabledCallbacks()) {
onBackPressedDispatcher.onBackPressed()
}
}
}

View File

@ -1,18 +1,9 @@
package de.mm20.launcher2.ui.launcher
import android.app.Activity
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.ColorScheme
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.ScreenOrientation
@ -21,7 +12,6 @@ import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.ui.GestureSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.gestures.Gesture
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -29,7 +19,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -37,8 +26,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
private val uiSettings: UiSettings by inject()
private val gestureSettings: GestureSettings by inject()
private val globalActionsService: GlobalActionsService by inject()
private val permissionsManager: PermissionsManager by inject()
private val searchableRepository: SavableSearchableRepository by inject()
private var isSystemInDarkMode = MutableStateFlow(false)
@ -69,8 +56,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
isSystemInDarkMode.value = darkMode
}
val baseLayout = uiSettings.baseLayout
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val bottomSearchBar = uiSettings.bottomSearchBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val reverseSearchResults = uiSettings.reverseSearchResults
@ -81,49 +66,11 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
.map { it != ScreenOrientation.Auto }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val isSearchOpen = mutableStateOf(false)
val isWidgetEditMode = mutableStateOf(false)
val searchBarFocused = mutableStateOf(false)
val widgetsOnHomeScreen = uiSettings.homeScreenWidgets
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val autoFocusSearch = uiSettings.openKeyboardOnSearch
fun setSearchbarFocus(focused: Boolean) {
if (searchBarFocused.value != focused) searchBarFocused.value = focused
}
fun openSearch() {
if (isSearchOpen.value == true) return
isSearchOpen.value = true
viewModelScope.launch {
if (autoFocusSearch.first()) setSearchbarFocus(true)
}
}
fun closeSearch() {
if (!isSearchOpen.value) return
isSearchOpen.value = false
setSearchbarFocus(false)
}
var skipNextSearchAnimation = false
fun closeSearchWithoutAnimation() {
if (!isSearchOpen.value) return
skipNextSearchAnimation = true
isSearchOpen.value = false
setSearchbarFocus(false)
}
fun toggleSearch() {
if (isSearchOpen.value == true) closeSearch()
else openSearch()
}
fun setWidgetEditMode(editMode: Boolean) {
isSearchOpen.value = false
isWidgetEditMode.value = editMode
}
val wallpaperBlur = uiSettings.blurWallpaper
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
val wallpaperBlurRadius = uiSettings.wallpaperBlurRadius
@ -137,34 +84,27 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
val searchBarStyle = uiSettings.searchBarStyle
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchBarStyle.Transparent)
val gestureState: StateFlow<GestureState> = gestureSettings
.combine(baseLayout) { settings, layout ->
val swipeLeftAction =
settings.swipeLeft.takeIf { layout != BaseLayout.Pager } ?: GestureAction.NoAction
val swipeRightAction = settings.swipeRight.takeIf { layout != BaseLayout.PagerReversed }
?: GestureAction.NoAction
val swipeDownAction =
settings.swipeDown.takeIf { layout != BaseLayout.PullDown } ?: GestureAction.NoAction
val gestureState: StateFlow<GestureState?> = gestureSettings.map { settings ->
val swipeLeftAction = settings.swipeLeft
val swipeRightAction = settings.swipeRight
val swipeDownAction = settings.swipeDown
val swipeUpAction = settings.swipeUp
val longPressAction = settings.longPress
val doubleTapAction = settings.doubleTap
val homeButtonAction = settings.homeButton
val swipeLeftAppKey =
if (swipeLeftAction is GestureAction.Launch) swipeLeftAction.key else null
val swipeRightAppKey =
if (swipeRightAction is GestureAction.Launch) swipeRightAction.key else null
val swipeDownAppKey =
if (swipeDownAction is GestureAction.Launch) swipeDownAction.key else null
val longPressAppKey =
if (longPressAction is GestureAction.Launch) longPressAction.key else null
val doubleTapAppKey =
if (doubleTapAction is GestureAction.Launch) doubleTapAction.key else null
val homeButtonAppKey =
if (homeButtonAction is GestureAction.Launch) homeButtonAction.key else null
val swipeLeftAppKey = (swipeLeftAction as? GestureAction.Launch)?.key
val swipeRightAppKey = (swipeRightAction as? GestureAction.Launch)?.key
val swipeDownAppKey = (swipeDownAction as? GestureAction.Launch)?.key
val swipeUpAppKey = (swipeUpAction as? GestureAction.Launch)?.key
val longPressAppKey = (longPressAction as? GestureAction.Launch)?.key
val doubleTapAppKey = (doubleTapAction as? GestureAction.Launch)?.key
val homeButtonAppKey = (homeButtonAction as? GestureAction.Launch)?.key
val apps = listOfNotNull(
swipeLeftAppKey,
swipeRightAppKey,
swipeDownAppKey,
swipeUpAppKey,
longPressAppKey,
doubleTapAppKey,
homeButtonAppKey,
@ -174,117 +114,35 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
swipeLeftAction = swipeLeftAction,
swipeRightAction = swipeRightAction,
swipeDownAction = swipeDownAction,
swipeUpAction = swipeUpAction,
longPressAction = longPressAction,
doubleTapAction = doubleTapAction,
homeButtonAction = homeButtonAction,
swipeLeftApp = apps.firstOrNull { it.key == swipeLeftAppKey },
swipeRightApp = apps.firstOrNull { it.key == swipeRightAppKey },
swipeDownApp = apps.firstOrNull { it.key == swipeDownAppKey },
longPressApp = apps.firstOrNull { it.key == longPressAppKey },
doubleTapApp = apps.firstOrNull { it.key == doubleTapAppKey },
homeButtonApp = apps.firstOrNull { it.key == homeButtonAppKey },
swipeLeftApp = apps.find { it.key == swipeLeftAppKey },
swipeRightApp = apps.find { it.key == swipeRightAppKey },
swipeDownApp = apps.find { it.key == swipeDownAppKey },
swipeUpApp = apps.find { it.key == swipeUpAppKey },
longPressApp = apps.find { it.key == longPressAppKey },
doubleTapApp = apps.find { it.key == doubleTapAppKey },
homeButtonApp = apps.find { it.key == homeButtonAppKey },
)
}.stateIn(viewModelScope, SharingStarted.Eagerly, GestureState())
var failedGestureState by mutableStateOf<FailedGesture?>(null)
fun handleGesture(context: Context, gesture: Gesture): Boolean {
val action = when (gesture) {
Gesture.DoubleTap -> gestureState.value.doubleTapAction
Gesture.LongPress -> gestureState.value.longPressAction
Gesture.SwipeDown -> gestureState.value.swipeDownAction.takeIf { baseLayout.value != BaseLayout.PullDown }
Gesture.SwipeLeft -> gestureState.value.swipeLeftAction.takeIf { baseLayout.value != BaseLayout.Pager }
Gesture.SwipeRight -> gestureState.value.swipeRightAction.takeIf { baseLayout.value != BaseLayout.PagerReversed }
Gesture.HomeButton -> gestureState.value.homeButtonAction
}
val requiresAccessibilityService =
action is GestureAction.Recents
|| action is GestureAction.PowerMenu
|| action is GestureAction.QuickSettings
|| action is GestureAction.Notifications
|| action is GestureAction.ScreenLock
if (action != null && requiresAccessibilityService && !permissionsManager.checkPermissionOnce(
PermissionGroup.Accessibility
)
) {
failedGestureState = FailedGesture(gesture, action)
return true
}
return when (action) {
is GestureAction.Search -> {
openSearch()
true
}
is GestureAction.Notifications -> {
globalActionsService.openNotificationDrawer()
true
}
is GestureAction.QuickSettings -> {
globalActionsService.openQuickSettings()
true
}
is GestureAction.ScreenLock -> {
globalActionsService.lockScreen()
true
}
is GestureAction.PowerMenu -> {
globalActionsService.openPowerDialog()
true
}
is GestureAction.Recents -> {
globalActionsService.openRecents()
true
}
is GestureAction.Launch -> {
val view = (context as Activity).window.decorView
val options = ActivityOptionsCompat.makeScaleUpAnimation(
view,
0,
0,
view.width,
view.height
)
when (gesture) {
Gesture.SwipeLeft -> gestureState.value.swipeLeftApp
Gesture.SwipeRight -> gestureState.value.swipeRightApp
Gesture.SwipeDown -> gestureState.value.swipeDownApp
Gesture.LongPress -> gestureState.value.longPressApp
Gesture.DoubleTap -> gestureState.value.doubleTapApp
Gesture.HomeButton -> gestureState.value.homeButtonApp
}?.launch(context, options.toBundle())
true
}
else -> false
}
}
fun dismissGestureFailedSheet() {
failedGestureState = null
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
data class GestureState(
val swipeLeftAction: GestureAction = GestureAction.NoAction,
val swipeRightAction: GestureAction = GestureAction.NoAction,
val swipeDownAction: GestureAction = GestureAction.NoAction,
val swipeUpAction: GestureAction = GestureAction.NoAction,
val longPressAction: GestureAction = GestureAction.NoAction,
val doubleTapAction: GestureAction = GestureAction.NoAction,
val homeButtonAction: GestureAction = GestureAction.NoAction,
val swipeLeftApp: SavableSearchable? = null,
val swipeRightApp: SavableSearchable? = null,
val swipeDownApp: SavableSearchable? = null,
val swipeUpApp: SavableSearchable? = null,
val longPressApp: SavableSearchable? = null,
val doubleTapApp: SavableSearchable? = null,
val homeButtonApp: SavableSearchable? = null,
)
data class FailedGesture(val gesture: Gesture, val action: GestureAction)

View File

@ -1,785 +0,0 @@
package de.mm20.launcher2.ui.launcher
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerSnapDistance
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Done
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.gestures.LauncherGestureHandler
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.pow
import kotlin.math.roundToInt
@Composable
fun PagerScaffold(
modifier: Modifier = Modifier,
darkStatusBarIcons: Boolean = false,
darkNavBarIcons: Boolean = false,
reverse: Boolean = false,
bottomSearchBar: Boolean = true,
reverseSearchResults: Boolean = true,
fixedSearchBar: Boolean = false,
) {
val viewModel: LauncherScaffoldVM = viewModel()
val searchVM: SearchVM = viewModel()
val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
val isSearchOpen by viewModel.isSearchOpen
val isWidgetEditMode by viewModel.isWidgetEditMode
val actions = searchVM.searchActionResults
val widgetsScrollState = rememberScrollState()
val searchState = rememberLazyListState()
val pagerState = rememberPagerState { 2 }
val filterBar by searchVM.filterBar.collectAsState(false)
val keyboardFilterBarPadding by animateDpAsState(
if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value && filterBar) 50.dp else 0.dp
)
val isSearchAtBottom by remember {
derivedStateOf {
if (reverseSearchResults) {
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
} else {
val lastItem =
searchState.layoutInfo.visibleItemsInfo.lastOrNull()
?: return@derivedStateOf true
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
}
}
}
val isSearchAtTop by remember {
derivedStateOf {
if (reverseSearchResults) {
val lastItem =
searchState.layoutInfo.visibleItemsInfo.lastOrNull()
?: return@derivedStateOf true
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
} else {
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
}
}
}
val showStatusBarScrim by remember {
derivedStateOf {
if (isSearchOpen) {
!isSearchAtTop
} else {
widgetsScrollState.value > 0
}
}
}
val fillClockHeight by viewModel.fillClockHeight.collectAsState()
val showNavBarScrim by remember {
derivedStateOf {
if (isSearchOpen) {
!isSearchAtBottom
} else {
(widgetsScrollState.value > 0 || !fillClockHeight) && widgetsScrollState.value < widgetsScrollState.maxValue
}
}
}
val isWidgetsScrollZero by remember {
derivedStateOf {
widgetsScrollState.value == 0
}
}
val systemUiController = rememberSystemUiController()
val colorSurface = MaterialTheme.colorScheme.surface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(
isWidgetEditMode,
darkStatusBarIcons,
colorSurface,
showStatusBarScrim,
isSearchOpen
) {
if (isWidgetEditMode) {
systemUiController.setStatusBarColor(
colorSurface
)
} else if (showStatusBarScrim) {
systemUiController.setStatusBarColor(
colorSurface.copy(0.7f),
)
} else if (isSearchOpen) {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = !isDarkTheme,
)
} else {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = darkStatusBarIcons
)
}
}
LaunchedEffect(darkNavBarIcons, showNavBarScrim) {
if (showNavBarScrim) {
systemUiController.setNavigationBarColor(
colorSurface.copy(0.7f),
)
} else {
systemUiController.setNavigationBarColor(
Color.Transparent,
darkIcons = darkNavBarIcons,
navigationBarContrastEnforced = false
)
}
}
val blurEnabled by viewModel.wallpaperBlur.collectAsState()
val blurRadius by viewModel.wallpaperBlurRadius.collectAsState()
val blurWallpaper by remember {
derivedStateOf {
blurEnabled && (isSearchOpen || !isWidgetsScrollZero)
}
}
WallpaperBlur {
if (blurWallpaper) blurRadius else 0
}
val currentPage = pagerState.currentPage
LaunchedEffect(currentPage) {
if (currentPage == 1) viewModel.openSearch()
else viewModel.closeSearch()
if (pagerState.currentPageOffsetFraction != 0f) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
}
LaunchedEffect(isSearchOpen) {
if (isSearchOpen) pagerState.animateScrollToPage(1)
else {
if (viewModel.skipNextSearchAnimation) {
pagerState.scrollToPage(0)
viewModel.skipNextSearchAnimation = false
} else {
pagerState.animateScrollToPage(0)
}
searchVM.reset()
}
}
val searchBarOffset = remember { mutableFloatStateOf(0f) }
val scope = rememberCoroutineScope()
val handleBackOrHomeEvent = {
when {
isSearchOpen -> {
viewModel.closeSearch()
searchVM.reset()
true
}
isWidgetEditMode -> {
viewModel.setWidgetEditMode(false)
true
}
widgetsScrollState.value != 0 -> {
scope.launch {
widgetsScrollState.animateScrollTo(0)
}
scope.launch {
searchBarOffset.animateTo(0f)
}
true
}
else -> false
}
}
BackHandler {
handleBackOrHomeEvent()
}
val gestureManager = LocalGestureDetector.current
val density = LocalDensity.current
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
val pagerNestedScrollConnection = remember(reverse) {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val drag = gestureManager.currentDrag
if (drag != null && drag.y > 0 && (reverse && drag.x < 0 || !reverse && drag.x > 0)) {
gestureManager.dispatchDrag(available)
return available
}
if (drag != null && drag.y > 0) {
gestureManager.dispatchDrag(available.copy(x = 0f))
return available.copy(x = 0f)
}
if (drag != null && (reverse && drag.x < 0 || !reverse && drag.x > 0)) {
gestureManager.dispatchDrag(available.copy(y = 0f))
return available.copy(y = 0f)
}
return super.onPreScroll(available, source)
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
if (source == NestedScrollSource.UserInput && !isWidgetEditMode && available != Offset.Zero) {
gestureManager.dispatchDrag(available)
}
val deltaSearchBarOffset =
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
searchBarOffset.floatValue =
(searchBarOffset.floatValue + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
return super.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
val drag = gestureManager.currentDrag
if (drag != null && drag.y > 0 && (reverse && drag.x < 0 || !reverse && drag.x > 0)) {
gestureManager.dispatchDragEnd()
return available
}
if (drag != null && drag.y > 0) {
gestureManager.dispatchDragEnd()
return available.copy(x = 0f)
}
if (drag != null && (reverse && drag.x < 0 || !reverse && drag.x > 0)) {
gestureManager.dispatchDragEnd()
return available.copy(y = 0f)
}
gestureManager.dispatchDragEnd()
return super.onPreFling(available)
}
}
}
val innerNestedScrollConnection = remember {
object : NestedScrollConnection {}
}
LaunchedEffect(pagerState.currentPage) {
searchBarOffset.animateTo(0f)
}
val searchNestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (source == NestedScrollSource.UserInput && available.y.absoluteValue > available.x.absoluteValue * 2) {
viewModel.setSearchbarFocus(false)
searchVM.bestMatch.value = null
}
return super.onPreScroll(available, source)
}
}
}
val insets = WindowInsets.safeDrawing.asPaddingValues()
Box(
modifier = modifier
) {
BoxWithConstraints(
modifier = Modifier.fillMaxWidth()
) {
val height by remember {
derivedStateOf { maxHeight }
}
val width by remember {
derivedStateOf { maxWidth }
}
CompositionLocalProvider(
LocalOverscrollConfiguration provides null,
) {
val minFlingVelocity = 1000.dp.toPixels()
val colorSurfaceContainer = MaterialTheme.colorScheme.surfaceContainer
val cardStyle = LocalCardStyle.current
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.drawBehind {
drawRect(
color = colorSurfaceContainer.copy(
alpha = -pagerState.getOffsetDistanceInPages(
0
) * 0.85f * cardStyle.opacity
),
)
}
.nestedScroll(pagerNestedScrollConnection),
beyondViewportPageCount = 1,
reverseLayout = reverse == (LocalLayoutDirection.current == LayoutDirection.Ltr),
state = pagerState,
userScrollEnabled = false,//!isWidgetEditMode,
flingBehavior = PagerDefaults.flingBehavior(
state = pagerState,
pagerSnapDistance = remember {
object : PagerSnapDistance {
override fun calculateTargetPage(
startPage: Int,
suggestedTargetPage: Int,
velocity: Float,
pageSize: Int,
pageSpacing: Int
): Int {
if (velocity.absoluteValue < minFlingVelocity) {
return startPage
}
return suggestedTargetPage
}
}
}
),
pageNestedScrollConnection = innerNestedScrollConnection,
) {
when (it) {
0 -> {
val editModePadding by animateDpAsState(if (isWidgetEditMode && bottomSearchBar) 56.dp else 0.dp)
val clockPadding by animateDpAsState(
if (isWidgetsScrollZero && fillClockHeight)
insets.calculateBottomPadding() + if (bottomSearchBar) 64.dp else 0.dp
else 0.dp
)
val clockHeight by remember {
derivedStateOf {
if (fillClockHeight) {
height - (64.dp + insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding)
} else {
null
}
}
}
Column(
modifier = Modifier
.requiredWidth(width)
.fillMaxHeight()
.pointerInput(gestureManager.shouldDetectDoubleTaps) {
detectTapGestures(
onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {
{
if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(
it
)
}
} else null,
onLongPress = {
if (!isWidgetEditMode) gestureManager.dispatchLongPress(
it
)
},
onTap = {
if (!isWidgetEditMode) gestureManager.dispatchTap(it)
},
)
}
.pagerScaffoldScrollHandler(
pagerState,
widgetsScrollState,
reversePager = reverse,
disablePager = isWidgetEditMode,
)
.verticalScroll(widgetsScrollState, enabled = false)
.windowInsetsPadding(WindowInsets.safeDrawing)
.graphicsLayer {
val pagerProgress =
pagerState.currentPage + pagerState.currentPageOffsetFraction
alpha = 1f - pagerProgress
}
.padding(8.dp)
.padding(
top = if (bottomSearchBar) 0.dp else 56.dp,
bottom = if (bottomSearchBar) 56.dp else 0.dp,
)
.padding(top = editModePadding)
) {
ClockWidget(
modifier = Modifier
.fillMaxWidth()
.then(clockHeight?.let { Modifier.height(it) } ?: Modifier)
.padding(bottom = clockPadding),
editMode = isWidgetEditMode,
fillScreenHeight = fillClockHeight,
)
WidgetColumn(
modifier = Modifier.fillMaxWidth(),
editMode = isWidgetEditMode,
onEditModeChange = {
viewModel.setWidgetEditMode(it)
}
)
}
}
1 -> {
val webSearchPadding by animateDpAsState(
if (actions.isEmpty()) 0.dp else 48.dp
)
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
val paddingValues = if (bottomSearchBar) {
PaddingValues(
top = 8.dp + windowInsets.calculateTopPadding(),
bottom = 64.dp + webSearchPadding + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
)
} else {
PaddingValues(
bottom = 8.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding,
top = 64.dp + webSearchPadding + windowInsets.calculateTopPadding()
)
}
SearchColumn(
modifier = Modifier
.requiredWidth(width)
.fillMaxHeight()
.graphicsLayer {
val pagerProgress =
pagerState.currentPage + pagerState.currentPageOffsetFraction
alpha = pagerProgress
}
.nestedScroll(searchNestedScrollConnection)
.pagerScaffoldScrollHandler(
pagerState,
searchState,
reversePager = reverse,
reverseScroll = reverseSearchResults
)
.padding(
start = windowInsets.calculateStartPadding(
LocalLayoutDirection.current
),
end = windowInsets.calculateStartPadding(
LocalLayoutDirection.current
),
),
reverse = reverseSearchResults,
state = searchState,
paddingValues = paddingValues,
userScrollEnabled = false,
)
}
}
}
}
}
AnimatedVisibility(visible = isWidgetEditMode,
enter = slideIn { IntOffset(0, -it.height) },
exit = slideOut { IntOffset(0, -it.height) }
) {
CenterAlignedTopAppBar(
modifier = Modifier.systemBarsPadding(),
title = {
Text(stringResource(R.string.menu_edit_widgets))
},
navigationIcon = {
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
Icon(imageVector = Icons.Rounded.Done, contentDescription = stringResource(R.string.action_done))
}
},
)
}
val searchBarLevel by remember {
derivedStateOf {
when {
pagerState.currentPageOffsetFraction != 0f -> SearchBarLevel.Raised
!isSearchOpen && isWidgetsScrollZero && (fillClockHeight || !bottomSearchBar) -> SearchBarLevel.Resting
isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active
isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active
else -> SearchBarLevel.Raised
}
}
}
val focusSearchBar by viewModel.searchBarFocused
val widgetEditModeOffset by animateDpAsState(
(if (isWidgetEditMode) 128.dp else 0.dp) * (if (bottomSearchBar) 1 else -1)
)
val value by searchVM.searchQuery
val searchBarColor by viewModel.searchBarColor.collectAsState()
val searchBarStyle by viewModel.searchBarStyle.collectAsState()
val launchOnEnter by searchVM.launchOnEnter.collectAsState(false)
LauncherSearchBar(
modifier = Modifier
.fillMaxSize(),
style = searchBarStyle,
level = { searchBarLevel },
value = { value },
focused = focusSearchBar,
onFocusChange = {
if (it) viewModel.openSearch()
viewModel.setSearchbarFocus(it)
},
actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction,
isSearchOpen = isSearchOpen,
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
bottomSearchBar = bottomSearchBar,
searchBarOffset = {
(if (focusSearchBar || fixedSearchBar) 0 else searchBarOffset.value.toInt() * if (bottomSearchBar) 1 else -1) +
with(density) {
(widgetEditModeOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp)
.toPx()
.roundToInt()
}
},
onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) }
} else null
)
}
LauncherGestureHandler(
onHomeButtonPress = handleBackOrHomeEvent,
)
}
fun Modifier.pagerScaffoldScrollHandler(
pagerState: PagerState,
scrollableState: ScrollableState,
reversePager: Boolean = false,
reverseScroll: Boolean = false,
disablePager: Boolean = false,
) = composed {
val scope = rememberCoroutineScope()
val flingBehavior = ScrollableDefaults.flingBehavior()
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
val touchSlopSq = LocalViewConfiguration.current.touchSlop.pow(2)
this
.nestedScroll(DefaultNestedScrollConnection, nestedScrollDispatcher)
.pointerInput(scrollableState, pagerState, reversePager, reverseScroll, disablePager) {
val velocityTracker = VelocityTracker()
val lockScrollThreshold = 200.dp.toPx()
val pagerMultiplier = if (reversePager) 1f else -1f
val scrollMultiplier = if (reverseScroll) 1f else -1f
awaitEachGesture {
var overSlop = false
val initialDown =
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
val down =
if (scrollableState.isScrollInProgress || pagerState.isScrollInProgress) {
overSlop = true
scope.launch {
scrollableState.scrollBy(0f)
pagerState.scrollBy(0f)
}
initialDown
} else {
awaitFirstDown(requireUnconsumed = false)
}
velocityTracker.resetTracking()
velocityTracker.addPointerInputChange(down)
val canceled = !drag(down.id) {
if (it.isConsumed) return@drag
val totalDrag = down.position - it.position
if (!overSlop && totalDrag.getDistanceSquared() > touchSlopSq) {
overSlop = true
}
if (!overSlop) return@drag
val dragAmount = it
.positionChange()
.let {
if (it.x.absoluteValue > it.y.absoluteValue) it.copy(y = 0f) else it.copy(
x = 0f
)
}
it.consume()
velocityTracker.addPointerInputChange(it)
scope.launch {
val preConsumed = nestedScrollDispatcher.dispatchPreScroll(
dragAmount,
NestedScrollSource.UserInput
)
val available = dragAmount - preConsumed
val consumedY =
scrollableState.scrollBy(available.y * scrollMultiplier) * scrollMultiplier
val consumedX =
if (disablePager) 0f
else pagerState.scrollBy(available.x * pagerMultiplier) * pagerMultiplier
val totalConsumed =
Offset(preConsumed.x + consumedX, preConsumed.y + consumedY)
nestedScrollDispatcher.dispatchPostScroll(
totalConsumed,
dragAmount - totalConsumed,
NestedScrollSource.UserInput
)
}
}
val velocity = velocityTracker
.calculateVelocity()
if (canceled || velocity.x.absoluteValue > velocity.y.absoluteValue) {
scope.launch {
val preConsumed = nestedScrollDispatcher.dispatchPreFling(velocity)
val flingVelocity = (velocity - preConsumed).x
if (!disablePager) {
if (flingVelocity.absoluteValue > 400.dp.toPx()) {
if (flingVelocity * pagerMultiplier < 0) {
pagerState.animateScrollToPage(pagerState.settledPage - 1)
} else {
pagerState.animateScrollToPage(pagerState.settledPage + 1)
}
} else {
pagerState.animateScrollToPage(pagerState.settledPage)
}
}
nestedScrollDispatcher.dispatchPostFling(
velocity,
Velocity.Zero,
)
}
} else {
scope.launch {
val preConsumed = nestedScrollDispatcher.dispatchPreFling(velocity)
val flingVelocity = (velocity - preConsumed).y
var consumed = 0f
launch {
with(flingBehavior) {
scrollableState.scroll {
consumed =
performFling(flingVelocity * scrollMultiplier) * scrollMultiplier
}
}
val totalConsumed =
Velocity(preConsumed.x, preConsumed.y + consumed)
nestedScrollDispatcher.dispatchPostFling(
totalConsumed,
velocity - totalConsumed
)
}
launch {
pagerState.animateScrollToPage(pagerState.settledPage)
}
}
}
}
}
}
internal object DefaultNestedScrollConnection : NestedScrollConnection {}

View File

@ -1,633 +0,0 @@
package de.mm20.launcher2.ui.launcher
import android.util.Log
import android.view.HapticFeedbackConstants
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Done
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.gestures.LauncherGestureHandler
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.min
import kotlin.math.roundToInt
@Composable
fun PullDownScaffold(
modifier: Modifier = Modifier,
darkStatusBarIcons: Boolean = false,
darkNavBarIcons: Boolean = false,
bottomSearchBar: Boolean = false,
reverseSearchResults: Boolean = false,
fixedSearchBar: Boolean = false,
) {
val viewModel: LauncherScaffoldVM = viewModel()
val searchVM: SearchVM = viewModel()
val density = LocalDensity.current
val context = LocalContext.current
val actions = searchVM.searchActionResults
val isSearchOpen by viewModel.isSearchOpen
val isWidgetEditMode by viewModel.isWidgetEditMode
val widgetsScrollState = rememberScrollState()
val searchState = rememberLazyListState()
val pagerState = rememberPagerState { 2 }
val offsetY = remember { mutableFloatStateOf(0f) }
val maxOffset = with(density) { 64.dp.toPx() }
val toggleSearchThreshold = with(density) { 48.dp.toPx() }
val filterBar by searchVM.filterBar.collectAsState(false)
val keyboardFilterBarPadding by animateDpAsState(
if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value && filterBar) 50.dp else 0.dp
)
val isSearchAtTop by remember {
derivedStateOf {
if (reverseSearchResults) {
val lastItem =
searchState.layoutInfo.visibleItemsInfo.lastOrNull()
?: return@derivedStateOf true
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
} else {
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
}
}
}
val isSearchAtBottom by remember {
derivedStateOf {
if (reverseSearchResults) {
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
} else {
val lastItem =
searchState.layoutInfo.visibleItemsInfo.lastOrNull()
?: return@derivedStateOf true
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
}
}
}
val isOverThreshold by remember {
derivedStateOf {
offsetY.floatValue.absoluteValue > toggleSearchThreshold
}
}
val dragProgress by remember {
derivedStateOf {
(offsetY.floatValue.absoluteValue / toggleSearchThreshold).coerceAtMost(1f)
}
}
val systemUiController = rememberSystemUiController()
val isWidgetsAtStart by remember {
derivedStateOf {
widgetsScrollState.value == 0
}
}
val isWidgetsAtEnd by remember {
derivedStateOf {
widgetsScrollState.value >= widgetsScrollState.maxValue
}
}
val fillClockHeight by viewModel.fillClockHeight.collectAsState()
val showStatusBarScrim by remember {
derivedStateOf {
if (isSearchOpen) {
!isSearchAtTop
} else {
widgetsScrollState.value > 0
}
}
}
val showNavBarScrim by remember {
derivedStateOf {
if (isSearchOpen) {
!isSearchAtBottom
} else {
(widgetsScrollState.value > 0 || !fillClockHeight) && widgetsScrollState.value < widgetsScrollState.maxValue
}
}
}
val colorSurface = MaterialTheme.colorScheme.surface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim, isSearchOpen) {
if (isWidgetEditMode) {
systemUiController.setStatusBarColor(
colorSurface
)
} else if (showStatusBarScrim) {
systemUiController.setStatusBarColor(
colorSurface.copy(0.7f),
)
} else if (isSearchOpen) {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = !isDarkTheme,
)
} else {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = darkStatusBarIcons
)
}
}
LaunchedEffect(darkNavBarIcons, showNavBarScrim) {
if (showNavBarScrim) {
systemUiController.setNavigationBarColor(
colorSurface.copy(0.7f),
)
} else {
systemUiController.setNavigationBarColor(
Color.Transparent,
darkIcons = darkNavBarIcons,
navigationBarContrastEnforced = false
)
}
}
val searchBarOffset = remember { mutableFloatStateOf(0f) }
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
val blurEnabled by viewModel.wallpaperBlur.collectAsState()
val blurRadius by viewModel.wallpaperBlurRadius.collectAsState()
val blurWallpaper by remember {
derivedStateOf {
blurEnabled && (isSearchOpen || isOverThreshold || widgetsScrollState.value > 0)
}
}
WallpaperBlur {
if (blurWallpaper) blurRadius else 0
}
val scope = rememberCoroutineScope()
LaunchedEffect(isSearchOpen) {
launch {
searchBarOffset.animateTo(0f)
}
if (isSearchOpen) {
searchState.scrollToItem(0)
pagerState.animateScrollToPage(
1,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
)
} else {
searchVM.reset()
if (viewModel.skipNextSearchAnimation) {
pagerState.scrollToPage(0)
viewModel.skipNextSearchAnimation = false
} else {
pagerState.animateScrollToPage(
0,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
)
}
}
}
LaunchedEffect(isWidgetEditMode) {
if (!isWidgetEditMode) searchBarOffset.floatValue = 0f
}
val handleBackOrHomeEvent = {
when {
isSearchOpen -> {
viewModel.closeSearch()
searchVM.reset()
true
}
isWidgetEditMode -> {
viewModel.setWidgetEditMode(false)
true
}
widgetsScrollState.value != 0 -> {
scope.launch {
widgetsScrollState.animateScrollTo(0)
}
scope.launch {
searchBarOffset.animateTo(0f)
}
true
}
else -> false
}
}
BackHandler {
handleBackOrHomeEvent()
}
val gestureManager = LocalGestureDetector.current
val view = LocalView.current
LaunchedEffect(isOverThreshold) {
if (isOverThreshold) {
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
} else if (offsetY.floatValue != 0f) {
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (isWidgetEditMode || source != NestedScrollSource.Drag) return Offset.Zero
if (available.y.absoluteValue > available.x.absoluteValue * 2) {
viewModel.setSearchbarFocus(false)
searchVM.bestMatch.value = null
}
val canPullDown = if (isSearchOpen) {
isSearchAtTop
} else {
isWidgetsAtStart
}
val canPullUp = isSearchOpen && isSearchAtBottom
val consumed = when {
canPullUp && available.y < 0 || offsetY.floatValue < 0 -> {
val consumed = available.y
offsetY.floatValue = (offsetY.floatValue + (consumed * 0.5f)).coerceIn(-maxOffset, 0f)
consumed
}
canPullDown && available.y > 0 || offsetY.floatValue > 0 -> {
val consumed = available.y
offsetY.floatValue = (offsetY.floatValue + (consumed * 0.5f)).coerceIn(0f, maxOffset)
consumed
}
else -> 0f
}
return Offset(0f, consumed)
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
val deltaSearchBarOffset =
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
searchBarOffset.floatValue =
(searchBarOffset.floatValue + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
return super.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (offsetY.floatValue > toggleSearchThreshold || offsetY.floatValue < -toggleSearchThreshold) {
viewModel.toggleSearch()
}
if (!isWidgetEditMode) gestureManager.dispatchDragEnd()
if (offsetY.floatValue != 0f) {
offsetY.animateTo(0f)
return available
}
return Velocity.Zero
}
}
}
val insets = WindowInsets.safeDrawing.asPaddingValues()
val colorSurfaceContainer = MaterialTheme.colorScheme.surfaceContainer
val cardStyle = LocalCardStyle.current
Box(
modifier = modifier
.drawBehind {
drawRect(
color = colorSurfaceContainer.copy(alpha = -pagerState.getOffsetDistanceInPages(0) * 0.85f * cardStyle.opacity),
)
}
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
if (!isWidgetEditMode) gestureManager.dispatchDragEnd()
},
onHorizontalDrag = { _, dragAmount ->
if (!isWidgetEditMode) gestureManager.dispatchDrag(Offset(dragAmount, 0f))
}
)
}
.offset { IntOffset(0, offsetY.floatValue.toInt()) },
contentAlignment = Alignment.TopCenter
) {
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
) {
val height by remember {
derivedStateOf { maxHeight }
}
CompositionLocalProvider(
LocalOverscrollConfiguration provides null
) {
VerticalPager(
modifier = Modifier
.fillMaxSize(),
beyondViewportPageCount = 1,
state = pagerState,
reverseLayout = true,
userScrollEnabled = false,
pageNestedScrollConnection = nestedScrollConnection,
) {
when (it) {
0 -> {
val clockPadding by animateDpAsState(
if (isWidgetsAtStart && fillClockHeight)
insets.calculateBottomPadding() + if (bottomSearchBar) 64.dp else 0.dp
else 0.dp
)
val editModePadding by animateDpAsState(if (isWidgetEditMode && bottomSearchBar) 56.dp else 0.dp)
val clockHeight by remember {
derivedStateOf {
if (fillClockHeight) {
height - (insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding + 56.dp)
} else {
null
}
}
}
Column(
modifier = Modifier
.graphicsLayer {
val progress =
pagerState.currentPage + pagerState.currentPageOffsetFraction
transformOrigin = TransformOrigin.Center
alpha = 1 - progress
scaleX = 1f - (dragProgress * 0.05f)
scaleY = 1f - (dragProgress * 0.05f)
}
.pointerInput(gestureManager.shouldDetectDoubleTaps) {
detectTapGestures(
onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {
{
if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(
it
)
}
} else null,
onLongPress = {
if (!isWidgetEditMode) gestureManager.dispatchLongPress(
it
)
},
onTap = {
if (!isWidgetEditMode) gestureManager.dispatchTap(it)
},
)
}
.fillMaxSize()
.verticalScroll(widgetsScrollState)
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(8.dp)
.padding(
top = if (bottomSearchBar) 0.dp else 56.dp,
bottom = if (bottomSearchBar) 56.dp else 0.dp,
)
.padding(top = editModePadding)
) {
ClockWidget(
modifier = Modifier
.fillMaxWidth()
.then(clockHeight?.let { Modifier.height(it) } ?: Modifier)
.padding(bottom = clockPadding),
editMode = isWidgetEditMode,
fillScreenHeight = fillClockHeight,
)
WidgetColumn(
modifier = Modifier.fillMaxWidth(),
editMode = isWidgetEditMode,
onEditModeChange = {
viewModel.setWidgetEditMode(it)
}
)
}
}
1 -> {
val webSearchPadding by animateDpAsState(
if (actions.isEmpty()) 0.dp else 48.dp
)
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
SearchColumn(
modifier = Modifier
.graphicsLayer {
val progress =
pagerState.currentPage + pagerState.currentPageOffsetFraction
transformOrigin = TransformOrigin.Center
alpha = min(progress, 1f - dragProgress * 0.1f)
scaleX = min(
1f - (dragProgress * 0.05f),
1f - (1f - progress) * 0.1f
)
scaleY = min(
1f - (dragProgress * 0.05f),
1f - (1f - progress) * 0.1f
)
}
.fillMaxSize()
.padding(
start = windowInsets.calculateStartPadding(
LocalLayoutDirection.current
),
end = windowInsets.calculateStartPadding(
LocalLayoutDirection.current
),
),
paddingValues = PaddingValues(
top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 64.dp + webSearchPadding else 8.dp,
bottom = windowInsets.calculateBottomPadding() +
keyboardFilterBarPadding +
if (bottomSearchBar) 64.dp + webSearchPadding else 8.dp
),
state = searchState,
reverse = reverseSearchResults,
)
}
}
}
}
}
AnimatedVisibility(visible = isWidgetEditMode,
enter = slideIn { IntOffset(0, -it.height) },
exit = slideOut { IntOffset(0, -it.height) }
) {
CenterAlignedTopAppBar(
modifier = Modifier
.windowInsetsPadding(WindowInsets.safeDrawing),
title = {
Text(stringResource(R.string.menu_edit_widgets))
},
navigationIcon = {
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
Icon(imageVector = Icons.Rounded.Done, contentDescription = stringResource(R.string.action_done))
}
}
)
}
val searchBarLevel by remember {
derivedStateOf {
when {
offsetY.floatValue != 0f -> SearchBarLevel.Raised
!isSearchOpen && isWidgetsAtStart && (fillClockHeight || !bottomSearchBar) -> SearchBarLevel.Resting
isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active
isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active
else -> SearchBarLevel.Raised
}
}
}
val searchBarFocused by viewModel.searchBarFocused
val editModeSearchBarOffset by animateDpAsState(
(if (isWidgetEditMode) 128.dp else 0.dp) * (if (bottomSearchBar) 1 else -1)
)
val value by searchVM.searchQuery
val searchBarColor by viewModel.searchBarColor.collectAsState()
val searchBarStyle by viewModel.searchBarStyle.collectAsState()
val launchOnEnter by searchVM.launchOnEnter.collectAsState(false)
LauncherSearchBar(
modifier = Modifier
.fillMaxSize(),
style = searchBarStyle,
level = { searchBarLevel },
value = { value },
focused = searchBarFocused,
onFocusChange = {
if (it) viewModel.openSearch()
viewModel.setSearchbarFocus(it)
},
actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction,
isSearchOpen = isSearchOpen,
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
bottomSearchBar = bottomSearchBar,
searchBarOffset = {
(if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.floatValue.toInt() * (if (bottomSearchBar) 1 else -1)) +
with(density) {
(editModeSearchBarOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp)
.toPx()
.roundToInt()
}
},
onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) }
} else null
)
}
LauncherGestureHandler(
onHomeButtonPress = handleBackOrHomeEvent,
)
}

View File

@ -5,28 +5,27 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.IntOffset
@ -34,23 +33,41 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.ui.assistant.AssistantScaffold
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCompositionLocals
import de.mm20.launcher2.ui.component.NavBarEffects
import de.mm20.launcher2.ui.gestures.GestureDetector
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.scaffold.ClockAndWidgetsHomeComponent
import de.mm20.launcher2.ui.launcher.scaffold.ClockHomeComponent
import de.mm20.launcher2.ui.launcher.scaffold.DismissComponent
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
import de.mm20.launcher2.ui.launcher.scaffold.LaunchComponent
import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffold
import de.mm20.launcher2.ui.launcher.scaffold.NotificationsComponent
import de.mm20.launcher2.ui.launcher.scaffold.PowerMenuComponent
import de.mm20.launcher2.ui.launcher.scaffold.QuickSettingsComponent
import de.mm20.launcher2.ui.launcher.scaffold.RecentsComponent
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldAnimation
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldConfiguration
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldGesture
import de.mm20.launcher2.ui.launcher.scaffold.ScreenOffComponent
import de.mm20.launcher2.ui.launcher.scaffold.SearchBarPosition
import de.mm20.launcher2.ui.launcher.scaffold.SearchComponent
import de.mm20.launcher2.ui.launcher.scaffold.SecretComponent
import de.mm20.launcher2.ui.launcher.scaffold.WidgetsComponent
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransition
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionManager
import de.mm20.launcher2.ui.launcher.transitions.LocalEnterHomeTransitionManager
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
@ -66,13 +83,15 @@ abstract class SharedLauncherActivity(
) : BaseActivity() {
private val viewModel: LauncherScaffoldVM by viewModels()
private val searchVM: SearchVM by viewModels()
internal val enterHomeTransitionManager = EnterHomeTransitionManager()
internal val gestureDetector = GestureDetector()
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
if (isAtLeastApiLevel(29)) {
window.isNavigationBarContrastEnforced = false
window.isStatusBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val wallpaperManager = WallpaperManager.getInstance(this)
@ -98,7 +117,6 @@ abstract class SharedLauncherActivity(
LocalWallpaperColors provides wallpaperColors,
LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText),
LocalBottomSheetManager provides bottomSheetManager,
LocalGestureDetector provides gestureDetector,
) {
LauncherTheme {
ProvideCompositionLocals {
@ -114,13 +132,20 @@ abstract class SharedLauncherActivity(
val hideStatus by viewModel.hideStatusBar.collectAsState()
val hideNav by viewModel.hideNavBar.collectAsState()
val layout by viewModel.baseLayout.collectAsState(null)
val bottomSearchBar by viewModel.bottomSearchBar.collectAsState()
val reverseSearchResults by viewModel.reverseSearchResults.collectAsState()
val fixedSearchBar by viewModel.fixedSearchBar.collectAsState()
val gestures by viewModel.gestureState.collectAsState()
val searchBarStyle by viewModel.searchBarStyle.collectAsState()
val searchBarColor by viewModel.searchBarColor.collectAsState()
val widgetsOnHomeScreen by viewModel.widgetsOnHomeScreen.collectAsState()
val fixedRotation by viewModel.fixedRotation.collectAsState()
val backgroundColor = MaterialTheme.colorScheme.surfaceContainer
if (gestures == null || widgetsOnHomeScreen == null) return@ProvideCompositionLocals
LaunchedEffect(fixedRotation) {
requestedOrientation = if (fixedRotation) {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
@ -129,8 +154,25 @@ abstract class SharedLauncherActivity(
}
}
val darkTheme = LocalDarkTheme.current
val darkSearchBar = LocalPreferDarkContentOverWallpaper.current
&& searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark
val systemUiController = rememberSystemUiController()
LaunchedEffect(dimBackground && darkTheme) {
if (dimBackground && darkTheme) {
val windowAttributes = window.attributes
windowAttributes.flags =
windowAttributes.flags or WindowManager.LayoutParams.FLAG_DIM_BEHIND
window.attributes = windowAttributes
window.setDimAmount(0.3f)
} else {
val windowAttributes = window.attributes
windowAttributes.flags =
windowAttributes.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()
window.attributes = windowAttributes
window.setDimAmount(0f)
}
}
val enterTransitionProgress = remember { mutableStateOf(1f) }
var enterTransition by remember {
@ -153,39 +195,180 @@ abstract class SharedLauncherActivity(
}
}
LaunchedEffect(hideStatus) {
systemUiController.isStatusBarVisible = !hideStatus
}
LaunchedEffect(hideNav) {
systemUiController.isNavigationBarVisible = !hideNav
}
OverlayHost(
modifier = Modifier
.fillMaxSize()
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
if (chargingAnimation == true) {
NavBarEffects(modifier = Modifier.fillMaxSize())
}
val config = remember(
mode,
reverseSearchResults,
bottomSearchBar,
fixedSearchBar,
gestures,
searchBarStyle,
darkSearchBar,
backgroundColor,
lightStatus,
lightNav,
hideStatus,
hideNav,
widgetsOnHomeScreen,
) {
if (mode == LauncherActivityMode.Assistant) {
key(bottomSearchBar, reverseSearchResults) {
AssistantScaffold(
modifier = Modifier
.fillMaxSize(),
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
val searchComponent = SearchComponent(
reverse = reverseSearchResults,
)
val dismissComponent =
DismissComponent(this@SharedLauncherActivity)
ScaffoldConfiguration(
homeComponent = searchComponent,
searchComponent = searchComponent,
swipeDown = ScaffoldGesture(
component = dismissComponent,
animation = ScaffoldAnimation.Push
),
swipeUp = ScaffoldGesture(
component = dismissComponent,
animation = ScaffoldAnimation.Push
),
fixedSearchBar = fixedSearchBar,
searchBarStyle = SearchBarStyle.Solid,
searchBarPosition = if (bottomSearchBar) SearchBarPosition.Bottom else SearchBarPosition.Top,
finishOnBack = true,
backgroundColor = backgroundColor,
)
}
} else {
when (layout) {
BaseLayout.PullDown -> {
key(bottomSearchBar, reverseSearchResults) {
PullDownScaffold(
val searchComponent = SearchComponent(
reverse = reverseSearchResults,
)
val widgetComponent by lazy { WidgetsComponent }
fun getScaffoldGesture(
action: GestureAction?,
searchable: SavableSearchable?,
gesture: Gesture
): ScaffoldGesture? {
return when (action) {
is GestureAction.Search -> ScaffoldGesture(
component = searchComponent,
animation = when (gesture) {
Gesture.SwipeDown -> ScaffoldAnimation.Rubberband
Gesture.LongPress -> ScaffoldAnimation.ZoomIn
Gesture.DoubleTap -> ScaffoldAnimation.ZoomIn
else -> ScaffoldAnimation.Push
},
)
is GestureAction.Widgets -> ScaffoldGesture(
component = widgetComponent,
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
is GestureAction.Notifications -> ScaffoldGesture(
component = NotificationsComponent,
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
is GestureAction.QuickSettings -> ScaffoldGesture(
component = QuickSettingsComponent,
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
is GestureAction.Recents -> ScaffoldGesture(
component = RecentsComponent,
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
is GestureAction.PowerMenu -> ScaffoldGesture(
component = PowerMenuComponent,
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
is GestureAction.ScreenLock -> ScaffoldGesture(
component = ScreenOffComponent,
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
is GestureAction.Launch if (searchable != null) -> ScaffoldGesture(
component = LaunchComponent(
this@SharedLauncherActivity,
searchable
),
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
)
else -> null
}
}
val gestures = gestures!!
val config = ScaffoldConfiguration(
homeComponent = if (widgetsOnHomeScreen == true) {
ClockAndWidgetsHomeComponent
} else {
ClockHomeComponent
},
searchComponent = searchComponent,
swipeUp = getScaffoldGesture(
gestures.swipeUpAction,
gestures.swipeUpApp,
Gesture.SwipeUp,
),
swipeDown = getScaffoldGesture(
gestures.swipeDownAction,
gestures.swipeDownApp,
Gesture.SwipeDown,
),
swipeLeft = getScaffoldGesture(
gestures.swipeLeftAction,
gestures.swipeLeftApp,
Gesture.SwipeLeft,
),
swipeRight = getScaffoldGesture(
gestures.swipeRightAction,
gestures.swipeRightApp,
Gesture.SwipeRight,
),
doubleTap = getScaffoldGesture(
gestures.doubleTapAction,
gestures.doubleTapApp,
Gesture.DoubleTap,
),
longPress = getScaffoldGesture(
gestures.longPressAction,
gestures.longPressApp,
Gesture.LongPress,
),
homeButton = getScaffoldGesture(
gestures.homeButtonAction,
gestures.homeButtonApp,
Gesture.HomeButton,
),
fixedSearchBar = fixedSearchBar,
searchBarStyle = searchBarStyle,
searchBarPosition = if (bottomSearchBar) SearchBarPosition.Bottom else SearchBarPosition.Top,
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
backgroundColor = backgroundColor,
showStatusBar = !hideStatus,
showNavBar = !hideNav,
darkSearchBar = darkSearchBar,
)
if (config.isUseless()) config.copy(
homeComponent = SecretComponent,
) else config
}
}
LauncherScaffold(
config = config,
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
@ -194,42 +377,9 @@ abstract class SharedLauncherActivity(
scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
}
)
}
}
BaseLayout.Pager,
BaseLayout.PagerReversed -> {
key(bottomSearchBar, reverseSearchResults) {
PagerScaffold(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX =
0.5f + enterTransitionProgress.value * 0.5f
scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
reverse = layout == BaseLayout.PagerReversed,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
}
else -> {}
}
}
SnackbarHost(
snackbarHostState,
modifier = Modifier
@ -270,20 +420,6 @@ abstract class SharedLauncherActivity(
}
}
private var pauseTime = 0L
override fun onPause() {
super.onPause()
pauseTime = System.currentTimeMillis()
}
override fun onResume() {
super.onResume()
if (System.currentTimeMillis() - pauseTime > 20000) {
viewModel.closeSearchWithoutAnimation()
searchVM.reset()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val windowController = WindowCompat.getInsetsController(window, window.decorView.rootView)

View File

@ -1,240 +0,0 @@
package de.mm20.launcher2.ui.launcher.gestures
import android.app.WallpaperManager
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
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.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.component.FakeSplashScreen
import de.mm20.launcher2.ui.gestures.Gesture
import de.mm20.launcher2.ui.gestures.GestureHandler
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.GestureState
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
import de.mm20.launcher2.ui.launcher.sheets.FailedGestureSheet
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
/**
* Handles gestures on the launcher according to the user's settings.
* @param onHomeButtonPress Called when the home button is pressed. Allows the caller to intercept the event.
* If the function returns true, the event is considered handled and the default action is not performed.
*/
@Composable
fun LauncherGestureHandler(
onHomeButtonPress: () -> Boolean = { false }
) {
val context = LocalContext.current
val wallpaperManager = remember { WallpaperManager.getInstance(context) }
val gestureDetector = LocalGestureDetector.current
val viewModel: LauncherScaffoldVM = viewModel()
val scope = rememberCoroutineScope()
val gestureState by viewModel.gestureState.collectAsState(GestureState())
val shouldDetectDoubleTapGesture = gestureState.doubleTapAction !is GestureAction.NoAction
LaunchedEffect(shouldDetectDoubleTapGesture) {
gestureDetector.shouldDetectDoubleTaps = shouldDetectDoubleTapGesture
}
val windowToken = LocalView.current.windowToken
var launchingApp by remember { mutableStateOf<SavableSearchable?>(null) }
var swipeGestureProgress = remember { mutableStateOf(0f) }
var swipeGestureDirection by remember { mutableStateOf<SwipeDirection?>(null) }
// Min swipe distance to trigger show the launch app preview
val swipeStartThreshold = 18.dp.toPixels()
// Min swipe distance to trigger the action
val swipeActionThreshold = 150.dp.toPixels()
GestureHandler(
detector = gestureDetector,
onDoubleTap = {
viewModel.handleGesture(context, Gesture.DoubleTap)
},
onLongPress = {
viewModel.handleGesture(context, Gesture.LongPress)
},
onHomeButtonPress = {
if (onHomeButtonPress()) {
return@GestureHandler
}
viewModel.handleGesture(context, Gesture.HomeButton)
},
onDrag = {
when {
gestureState.swipeRightApp != null && it.x > swipeStartThreshold && (
swipeGestureDirection == SwipeDirection.Right || (swipeGestureDirection == null && it.x.absoluteValue > it.y.absoluteValue * 2f)
) -> {
swipeGestureDirection = SwipeDirection.Right
swipeGestureProgress.value =
((it.x - swipeStartThreshold) / (swipeActionThreshold - swipeStartThreshold)).coerceIn(
0f,
2f
)
launchingApp = gestureState.swipeRightApp
return@GestureHandler false
}
gestureState.swipeLeftApp != null && it.x < -swipeStartThreshold && (
swipeGestureDirection == SwipeDirection.Left || (swipeGestureDirection == null && it.x.absoluteValue > it.y.absoluteValue * 2f)
) -> {
swipeGestureDirection = SwipeDirection.Left
swipeGestureProgress.value =
((-it.x - swipeStartThreshold) / (swipeActionThreshold - swipeStartThreshold)).coerceIn(
0f,
2f
)
launchingApp = gestureState.swipeLeftApp
return@GestureHandler false
}
gestureState.swipeDownApp != null && it.y > swipeStartThreshold && (
swipeGestureDirection == SwipeDirection.Down || (swipeGestureDirection == null && it.y.absoluteValue > it.x.absoluteValue * 2f)
) -> {
swipeGestureDirection = SwipeDirection.Down
swipeGestureProgress.value =
((it.y - swipeStartThreshold) / (swipeActionThreshold - swipeStartThreshold)).coerceIn(
0f,
2f
)
launchingApp = gestureState.swipeDownApp
return@GestureHandler false
}
else -> {
swipeGestureDirection = null
swipeGestureProgress.value = 0f
launchingApp = null
}
}
return@GestureHandler when {
it.x > swipeActionThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
viewModel.handleGesture(context, Gesture.SwipeRight)
}
it.x < -swipeActionThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
viewModel.handleGesture(context, Gesture.SwipeLeft)
}
it.y > swipeActionThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> {
viewModel.handleGesture(context, Gesture.SwipeDown)
}
else -> false
}
},
onDragEnd = {
if (swipeGestureProgress.value > 0f) {
scope.launch {
val direction = swipeGestureDirection
if (swipeGestureProgress.value >= 1f
&& direction != null
) {
swipeGestureProgress.animateTo(2f)
viewModel.handleGesture(
context,
when (direction) {
SwipeDirection.Right -> Gesture.SwipeRight
SwipeDirection.Left -> Gesture.SwipeLeft
SwipeDirection.Down -> Gesture.SwipeDown
},
)
delay(500)
} else {
swipeGestureProgress.animateTo(0f)
}
swipeGestureDirection = null
swipeGestureProgress.value = 0f
launchingApp = null
}
}
},
onTap = {
wallpaperManager.sendWallpaperCommand(
windowToken,
WallpaperManager.COMMAND_TAP,
it.x.toInt(),
it.y.toInt(),
0,
null
)
}
)
if (launchingApp != null) {
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
FakeSplashScreen(
modifier = Modifier
.offset {
val p = swipeGestureProgress.value
when (swipeGestureDirection) {
SwipeDirection.Right -> IntOffset(
(-minWidth.toPx() * (1f - p * 0.5f)).toInt(),
0
)
SwipeDirection.Left -> IntOffset(
(minWidth.toPx() * (1f - p * 0.5f)).toInt(),
0
)
SwipeDirection.Down -> IntOffset(
0,
(-minHeight.toPx() * (1f - p * 0.5f)).toInt()
)
else -> IntOffset.Zero
}
}
.graphicsLayer {
alpha = (swipeGestureProgress.value).coerceAtMost(1f)
},
searchable = launchingApp,
)
}
}
if (viewModel.failedGestureState != null) {
FailedGestureSheet(
failedGesture = viewModel.failedGestureState!!,
onDismiss = {
viewModel.dismissGestureFailedSheet()
}
)
}
}
internal enum class SwipeDirection {
Left,
Right,
Down
}

View File

@ -0,0 +1,143 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import kotlinx.coroutines.launch
internal object ClockAndWidgetsHomeComponent: ScaffoldComponent() {
private var editMode by mutableStateOf(false)
private val scrollState = ScrollState(0)
override val isAtTop: State<Boolean?> = derivedStateOf {
!scrollState.canScrollBackward
}
override val isAtBottom: State<Boolean?> = derivedStateOf {
!scrollState.canScrollForward
}
override val drawBackground: Boolean = false
// In note widget
override val hasIme: Boolean = true
override val showSearchBar: Boolean = false
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
val scope = rememberCoroutineScope()
val topPadding by animateDpAsState(if (editMode) 80.dp else 0.dp)
val previousScroll = remember { mutableIntStateOf(scrollState.value) }
LaunchedEffect(scrollState.value, scrollState.canScrollForward, scrollState.canScrollBackward) {
val delta = scrollState.value - previousScroll.intValue
previousScroll.intValue = scrollState.value
if (!editMode) {
state.onComponentScroll(delta.toFloat())
}
}
Column(
modifier = modifier
.verticalScroll(scrollState, enabled = !state.isDragged)
.padding(horizontal = 8.dp)
.padding(top = topPadding)
.padding(insets),
) {
ClockWidget(
editMode = editMode,
fillScreenHeight = false,
)
WidgetColumn(
modifier = Modifier
.padding(top = 16.dp),
editMode = editMode,
onEditModeChange = {
scope.launch { state.lock(hideSearchBar = true) }
editMode = it
},
)
}
if (editMode) {
BackHandler {
editMode = false
scope.launch { state.unlock() }
}
}
AnimatedVisibility(
editMode,
modifier = Modifier.zIndex(10f),
enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
CenterAlignedTopAppBar(
title = { Text(stringResource(R.string.menu_edit_widgets)) },
navigationIcon = {
IconButton(
onClick = {
editMode = false
scope.launch { state.unlock() }
}
) {
Icon(Icons.AutoMirrored.Rounded.ArrowBack, stringResource(R.string.action_done))
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
)
}
}
override suspend fun onDismiss(state: LauncherScaffoldState) {
super.onDismiss(state)
scrollState.scrollTo(0)
}
}

View File

@ -0,0 +1,32 @@
package de.mm20.launcher2.ui.launcher.scaffold
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
internal object ClockHomeComponent : ScaffoldComponent() {
override val drawBackground: Boolean = false
override var isAtTop: State<Boolean?> = mutableStateOf(true)
override var isAtBottom: State<Boolean?> = mutableStateOf(true)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState,
) {
ClockWidget(
modifier = modifier
.padding(insets)
.pointerInput(Unit) {},
fillScreenHeight = true,
)
}
}

View File

@ -0,0 +1,54 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import android.app.Activity
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
/**
* A scaffold component that finishes the activity when activated.
*/
internal class DismissComponent(private val activity: Activity) : ScaffoldComponent() {
override val drawBackground: Boolean = false
override val isAtTop: State<Boolean?> = mutableStateOf(true)
override val isAtBottom: State<Boolean?> = mutableStateOf(true)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return Modifier.alpha(1f - state.currentProgress) then defaultModifier then Modifier.scale(
1f - (state.currentProgress * 0.25f)
)
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.alpha(1f - state.currentProgress)
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
activity.finish()
}
}

View File

@ -0,0 +1,70 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import android.app.Activity
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.zIndex
import androidx.core.app.ActivityOptionsCompat
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.component.FakeSplashScreen
import de.mm20.launcher2.ui.ktx.toIntOffset
internal class LaunchComponent(
private val activity: Activity,
private val searchable: SavableSearchable,
): ScaffoldComponent() {
override val permanent: Boolean = false
override val resetDelay: Long = 500L
override val showSearchBar = false
override val isAtTop: State<Boolean?> = mutableStateOf(true)
override val isAtBottom: State<Boolean?> = mutableStateOf(true)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
FakeSplashScreen(
modifier = modifier.zIndex(10f).alpha((state.currentProgress * 2f).coerceAtMost(1f)),
searchable = searchable,
)
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
val view = activity.window.decorView
val options = ActivityOptionsCompat.makeClipRevealAnimation(
view,
0,
0,
view.width,
view.height
)
searchable.launch(activity, options.toBundle())
}
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.alpha(1f - state.currentProgress)
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.offset{ state.currentOffset.toIntOffset() }.alpha(1f - state.currentProgress)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,138 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import android.view.animation.PathInterpolator
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal object NotificationsComponent : ScaffoldComponent(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val globalActionService: GlobalActionsService by inject()
override val permanent: Boolean
get() = !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)
override val showSearchBar: Boolean = false
override val drawBackground: Boolean = false
private val interpolator = PathInterpolator(0f, 0f, 0f, 1f)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
if (isActive) {
val bottomSheetManager = LocalBottomSheetManager.current
LaunchedEffect(Unit) {
val gesture = state.currentGesture ?: return@LaunchedEffect
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
bottomSheetManager.showFailedGestureSheet(
gesture = gesture,
action = GestureAction.Notifications,
)
}
}
}
val scale by animateFloatAsState(
if (state.currentProgress >= 0.5f) 1.2f else 1f
)
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(99f)
.pointerInput(Unit) {},
contentAlignment = Alignment.TopCenter
) {
Box(
modifier = Modifier
.systemBarsPadding()
.padding(16.dp)
.size(64.dp)
.offset(
y = -134.dp * interpolator.getInterpolation(1f - state.currentProgress * 2f)
.coerceAtLeast(0f)
)
.scale(scale)
.shadow(4.dp, CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Rounded.Notifications, null,
modifier = Modifier.padding(16.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
if (permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
globalActionService.openNotificationDrawer()
} else {
state.navigateBack(true)
}
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return Modifier
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.composed {
val color = MaterialTheme.colorScheme.scrim
Modifier.drawWithContent {
drawContent()
drawRect(
color = color.copy(alpha = 0.5f * (state.currentProgress * 2f).coerceAtMost(1f))
)
}
}
}
}

View File

@ -0,0 +1,200 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import android.view.Surface
import android.view.animation.PathInterpolator
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PowerSettingsNew
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.compose.ui.zIndex
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal object PowerMenuComponent : ScaffoldComponent(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val globalActionService: GlobalActionsService by inject()
override val permanent: Boolean
get() = !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)
override val showSearchBar: Boolean = false
override val drawBackground: Boolean = false
private val interpolator = PathInterpolator(0f, 0f, 0f, 1f)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
val context = LocalContext.current
val view = LocalView.current
val rotation = view.display.rotation
val powerButtonY = remember {
val resources =
context.packageManager.getResourcesForApplication("com.android.systemui")
val resId = resources.getIdentifier(
"physical_power_button_center_screen_location_y",
"dimen",
"com.android.systemui"
)
if (resId != 0) {
resources.getDimensionPixelSize(resId)
} else {
(view.height * 0.33f).toInt()
}
}
if (isActive) {
val bottomSheetManager = LocalBottomSheetManager.current
LaunchedEffect(Unit) {
val gesture = state.currentGesture ?: return@LaunchedEffect
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
bottomSheetManager.showFailedGestureSheet(
gesture = gesture,
action = GestureAction.PowerMenu,
)
}
}
}
val scale by animateFloatAsState(
if (state.currentProgress >= 0.5f) 1.2f else 1f
)
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(99f)
.pointerInput(Unit) {},
contentAlignment = when (rotation) {
Surface.ROTATION_0 -> AbsoluteAlignment.TopRight
Surface.ROTATION_90 -> AbsoluteAlignment.TopLeft
Surface.ROTATION_180 -> AbsoluteAlignment.BottomLeft
Surface.ROTATION_270 -> AbsoluteAlignment.BottomRight
else -> AbsoluteAlignment.TopRight
}
) {
Box(
modifier = Modifier
.offset {
when (rotation) {
Surface.ROTATION_0 -> IntOffset(0, powerButtonY)
Surface.ROTATION_90 -> IntOffset(powerButtonY, 0)
Surface.ROTATION_180 -> IntOffset(0, -powerButtonY)
Surface.ROTATION_270 -> IntOffset(-powerButtonY, 0)
else -> IntOffset(0, powerButtonY)
}
}
.offset(
x = when (rotation) {
Surface.ROTATION_90 -> -48.dp
Surface.ROTATION_270 -> 48.dp
else -> 0.dp
},
y =
when (rotation) {
Surface.ROTATION_0 -> -48.dp
Surface.ROTATION_180 -> 48.dp
else -> 0.dp
},
)
.padding(16.dp)
.size(64.dp)
.offset(
x = when (rotation) {
Surface.ROTATION_0 -> 1
Surface.ROTATION_180 -> -1
else -> 0
} * 128.dp * interpolator.getInterpolation(1f - state.currentProgress * 2f)
.coerceAtLeast(0f),
y = when (rotation) {
Surface.ROTATION_90 -> -1
Surface.ROTATION_270 -> 1
else -> 0
} * 128.dp * interpolator.getInterpolation(1f - state.currentProgress * 2f)
.coerceAtLeast(0f),
)
.scale(scale)
.shadow(4.dp, CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Rounded.PowerSettingsNew, null,
modifier = Modifier.padding(16.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
if (permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
globalActionService.openPowerDialog()
} else {
state.navigateBack(true)
}
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return Modifier
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.composed {
val color = MaterialTheme.colorScheme.scrim
Modifier.drawWithContent {
drawContent()
drawRect(
color = color.copy(alpha = 0.5f * (state.currentProgress * 2f).coerceAtMost(1f))
)
}
}
}
}

View File

@ -0,0 +1,135 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import android.view.animation.PathInterpolator
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal object QuickSettingsComponent : ScaffoldComponent(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val globalActionService: GlobalActionsService by inject()
override val permanent: Boolean
get() = !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)
override val showSearchBar: Boolean = false
override val drawBackground: Boolean = false
private val interpolator = PathInterpolator(0f, 0f, 0f, 1f)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
if (isActive) {
val bottomSheetManager = LocalBottomSheetManager.current
LaunchedEffect(Unit) {
val gesture = state.currentGesture ?: return@LaunchedEffect
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
bottomSheetManager.showFailedGestureSheet(
gesture = gesture,
action = GestureAction.QuickSettings,
)
}
}
}
val scale by animateFloatAsState(
if (state.currentProgress >= 0.5f) 1.2f else 1f
)
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(99f)
.pointerInput(Unit) {},
contentAlignment = Alignment.TopCenter
) {
Box(
modifier = Modifier
.systemBarsPadding()
.padding(16.dp)
.size(64.dp)
.offset(y = -134.dp * interpolator.getInterpolation(1f - state.currentProgress * 2f).coerceAtLeast(0f))
.scale(scale)
.shadow(4.dp, CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Rounded.Settings, null,
modifier = Modifier.padding(16.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
if (permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
globalActionService.openQuickSettings()
} else {
state.navigateBack(true)
}
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return Modifier
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.composed {
val color = MaterialTheme.colorScheme.scrim
Modifier.drawWithContent {
drawContent()
drawRect(
color = color.copy(alpha = 0.5f * (state.currentProgress * 2f).coerceAtMost(1f))
)
}
}
}
}

View File

@ -0,0 +1,132 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import android.view.RoundedCorner
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import kotlinx.coroutines.delay
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal object RecentsComponent : ScaffoldComponent(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val globalActionService: GlobalActionsService by inject()
override val permanent: Boolean
get() = !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)
override val showSearchBar: Boolean = false
override val drawBackground: Boolean = false
override val isAtTop: State<Boolean?> = mutableStateOf(true)
override val isAtBottom: State<Boolean?> = mutableStateOf(true)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
if (isActive) {
val bottomSheetManager = LocalBottomSheetManager.current
LaunchedEffect(Unit) {
val gesture = state.currentGesture ?: return@LaunchedEffect
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
bottomSheetManager.showFailedGestureSheet(
gesture = gesture,
action = GestureAction.Recents,
)
}
}
}
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
if (permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
globalActionService.openRecents()
delay(50L)
state.navigateBack(true)
} else {
state.navigateBack(true)
}
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier = Modifier.composed {
val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val insets = LocalView.current.rootWindowInsets
val shape = if (isAtLeastApiLevel(31) && insets != null) {
RoundedCornerShape(
topStart = insets.getRoundedCorner(if (rtl) RoundedCorner.POSITION_TOP_RIGHT else RoundedCorner.POSITION_TOP_LEFT)?.radius?.toFloat()
?: 0f,
topEnd = insets.getRoundedCorner(if (rtl) RoundedCorner.POSITION_TOP_LEFT else RoundedCorner.POSITION_TOP_RIGHT)?.radius?.toFloat()
?: 0f,
bottomStart = insets.getRoundedCorner(if (rtl) RoundedCorner.POSITION_BOTTOM_RIGHT else RoundedCorner.POSITION_BOTTOM_LEFT)?.radius?.toFloat()
?: 0f,
bottomEnd = insets.getRoundedCorner(if (rtl) RoundedCorner.POSITION_BOTTOM_LEFT else RoundedCorner.POSITION_BOTTOM_RIGHT)?.radius?.toFloat()
?: 0f,
)
} else {
RectangleShape
}
val surfaceColor = MaterialTheme.colorScheme.surfaceContainer
Modifier
.drawWithContent() {
drawRect(color = surfaceColor.copy(alpha = state.currentProgress * 0.5f))
drawIntoCanvas {
withTransform({
this.scale(1f - state.currentProgress * 0.3f)
}) {
drawOutline(
shape.createOutline(size, layoutDirection, Density(density, fontScale)),
color = Color.Black,
blendMode = BlendMode.Clear
)
this@drawWithContent.drawContent()
}
}
}
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return defaultModifier.scale(1f - state.currentProgress * 0.3f)
}
}

View File

@ -0,0 +1,103 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
internal abstract class ScaffoldComponent {
/**
* If true, the component stays open. I.e. widgets, search.
* If false, the component is immediately dismissed after running its onMount function,
* returning to the home screen. I.e. turn off screen, launch app.
*/
open val permanent: Boolean = true
/**
* For non-permanent components, this is the delay before the component is dismissed.
*/
open val resetDelay: Long = 0L
/**
* If true, a semi-transparent background is drawn behind the page.
*/
open val drawBackground: Boolean = true
/**
* Show the search bar on this component, if search bar style is hidden.
* For other styles, the search bar is always shown.
*/
open val showSearchBar: Boolean = true
/**
* Whether haptic feedback should be used when the component is activated / dismissed.
*/
open val hapticFeedback: Boolean = true
/**
* Whether this component can be interacted with the IME.
* If true, IME insets will be passed to the component.
* If false, IME insets will be ignored, to avoid unnecessary animations.
*/
open val hasIme: Boolean = false
/**
* Whether the component is scrolled all the way up
* null, if the component does not provide content
*/
open val isAtTop: State<Boolean?> = mutableStateOf(null)
/**
* Whether the component is scrolled all the way down
* null, if the component does not provide content
*/
open val isAtBottom: State<Boolean?> = mutableStateOf(null)
@Composable abstract fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState,
)
@SuppressLint("ModifierFactoryExtensionFunction")
open fun homePageModifier(state: LauncherScaffoldState, defaultModifier: Modifier): Modifier = defaultModifier
@SuppressLint("ModifierFactoryExtensionFunction")
open fun searchBarModifier(state: LauncherScaffoldState, defaultModifier: Modifier): Modifier = defaultModifier
protected var isActive by mutableStateOf(false)
/**
* Called when the component is about to be activated, but before the animation is completed.
*/
open suspend fun onPreActivate(state: LauncherScaffoldState) {
}
/**
* Called when the component is activated, after the animation is completed.
*/
open suspend fun onActivate(state: LauncherScaffoldState) {
isActive = true
}
/**
* Called when the component is about to be dismissed, but before the animation is completed.
*/
open suspend fun onPreDismiss(state: LauncherScaffoldState) {
}
/**
* Called when the component is dismissed, after the animation is completed, when the component is no longer visible.
*/
open suspend fun onDismiss(state: LauncherScaffoldState) {
isActive = false
}
}

View File

@ -0,0 +1,106 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal object ScreenOffComponent : ScaffoldComponent(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val globalActionService: GlobalActionsService by inject()
override val permanent: Boolean
get() = !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)
override val showSearchBar: Boolean = false
override val resetDelay: Long = 1000L
override val drawBackground: Boolean = false
override val isAtTop: State<Boolean?> = mutableStateOf(true)
override val isAtBottom: State<Boolean?> = mutableStateOf(true)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
if (isActive) {
val bottomSheetManager = LocalBottomSheetManager.current
LaunchedEffect(Unit) {
val gesture = state.currentGesture ?: return@LaunchedEffect
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
bottomSheetManager.showFailedGestureSheet(
gesture = gesture,
action = GestureAction.ScreenLock,
)
}
}
}
Box(
modifier = modifier
.zIndex(10f)
.pointerInput(Unit) {},
contentAlignment = Alignment.Center
) {
}
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun homePageModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return Modifier
.scale(1f - (state.currentProgress * 0.1f))
.blur(12.dp * state.currentProgress)
.alpha(1f - (state.currentProgress * 0.1f))
}
@SuppressLint("ModifierFactoryExtensionFunction")
override fun searchBarModifier(
state: LauncherScaffoldState,
defaultModifier: Modifier
): Modifier {
return Modifier
.drawWithContent {
drawContent()
drawRect(Color.Black, alpha = state.currentProgress)
}
.scale(1f - (state.currentProgress * 0.1f))
.blur(12.dp * state.currentProgress)
.alpha(1f - (state.currentProgress * 0.1f)) then defaultModifier
}
override suspend fun onActivate(state: LauncherScaffoldState) {
super.onActivate(state)
if (permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) {
globalActionService.lockScreen()
} else {
state.onPredictiveBackEnd()
}
}
}

View File

@ -0,0 +1,94 @@
package de.mm20.launcher2.ui.launcher.scaffold
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM
internal class SearchComponent(
private val reverse: Boolean = false,
) : ScaffoldComponent() {
override val isAtTop: MutableState<Boolean?> = mutableStateOf(true)
override val isAtBottom: MutableState<Boolean?> = mutableStateOf(true)
override val hasIme: Boolean = true
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
val searchVM = viewModel<SearchVM>()
val lazyListState = rememberLazyListState()
LaunchedEffect(isActive) {
if (!isActive) {
searchVM.reset()
lazyListState.scrollToItem(0, 0)
}
}
LaunchedEffect(lazyListState.canScrollForward, lazyListState.canScrollBackward) {
isAtBottom.value = !lazyListState.canScrollForward && !reverse || !lazyListState.canScrollBackward && reverse
isAtTop.value = !lazyListState.canScrollForward && reverse || !lazyListState.canScrollBackward && !reverse
}
val scrollConnection = remember(state) {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
searchVM.bestMatch.value = null
state.isSearchBarFocused = false
state.onComponentScroll(
if (reverse) consumed.y else -consumed.y,
)
return super.onPostScroll(consumed, available, source)
}
}
}
SearchColumn(
modifier.nestedScroll(scrollConnection),
paddingValues = insets,
state = lazyListState,
reverse = reverse,
userScrollEnabled = !state.isDragged,
)
}
override suspend fun onDismiss(state: LauncherScaffoldState) {
super.onDismiss(state)
}
override suspend fun onPreActivate(state: LauncherScaffoldState) {
super.onPreActivate(state)
state.isSearchBarFocused = true
}
override suspend fun onPreDismiss(state: LauncherScaffoldState) {
super.onPreDismiss(state)
state.isSearchBarFocused = false
}
}

View File

@ -0,0 +1,82 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.content.Intent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.settings.SettingsActivity
internal object SecretComponent : ScaffoldComponent() {
override var isAtTop: State<Boolean?> = mutableStateOf(true)
override var isAtBottom: State<Boolean?> = mutableStateOf(true)
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
val context = LocalContext.current
Column(
modifier = modifier
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val color = MaterialTheme.colorScheme.onSurface
Text(
"\uD83E\uDEF5\uD83D\uDE06",
style = MaterialTheme.typography.displayLarge,
textAlign = TextAlign.Center
)
Text(
stringResource(R.string.bad_configuration_title),
color = color,
modifier = Modifier.padding(vertical = 16.dp),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center
)
Text(
stringResource(R.string.bad_configuration_summary),
color = color,
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
Button(
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
onClick = {
context.startActivity(Intent(context, SettingsActivity::class.java))
}, modifier = Modifier.padding(top = 24.dp)) {
Icon(
Icons.Rounded.Settings,
contentDescription = null,
modifier = Modifier.padding(ButtonDefaults.IconSpacing).size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.settings))
}
}
}
}

View File

@ -0,0 +1,134 @@
package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import kotlinx.coroutines.launch
internal object WidgetsComponent : ScaffoldComponent() {
private val scrollState = ScrollState(0)
override val isAtTop: State<Boolean?> = derivedStateOf {
!scrollState.canScrollBackward
}
override val isAtBottom: State<Boolean?> = derivedStateOf {
!scrollState.canScrollForward
}
// In note widget
override val hasIme: Boolean = true
@Composable
override fun Component(
modifier: Modifier,
insets: PaddingValues,
state: LauncherScaffoldState
) {
var editMode by rememberSaveable { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val topPadding by animateDpAsState(if (editMode) 64.dp else 0.dp)
val previousScroll = remember { mutableIntStateOf(scrollState.value) }
LaunchedEffect(scrollState.value, scrollState.canScrollForward, scrollState.canScrollBackward) {
val delta = scrollState.value - previousScroll.intValue
previousScroll.intValue = scrollState.value
if (!editMode) {
state.onComponentScroll(delta.toFloat())
}
}
Column(
modifier = modifier
.verticalScroll(scrollState)
.padding(horizontal = 8.dp)
.padding(top = topPadding)
.padding(insets),
) {
WidgetColumn(
modifier = Modifier,
editMode = editMode,
onEditModeChange = {
scope.launch { state.lock(hideSearchBar = true) }
editMode = it
},
)
}
if (editMode) {
BackHandler {
editMode = false
scope.launch { state.unlock() }
}
}
AnimatedVisibility(
editMode,
modifier = Modifier.zIndex(10f),
enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
CenterAlignedTopAppBar(
title = { Text(stringResource(R.string.menu_edit_widgets)) },
navigationIcon = {
IconButton(
onClick = {
editMode = false
scope.launch { state.unlock() }
}
) {
Icon(Icons.AutoMirrored.Rounded.ArrowBack, stringResource(R.string.action_done))
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
)
}
}
override suspend fun onDismiss(state: LauncherScaffoldState) {
super.onDismiss(state)
scrollState.scrollTo(0)
}
}

View File

@ -53,6 +53,7 @@ import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
@Composable
fun SearchColumn(
@ -369,7 +370,7 @@ fun LazyListScope.SingleResult(
vertical = 4.dp,
),
color = if (highlight) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surface.copy(LocalCardStyle.current.opacity)
else MaterialTheme.colorScheme.surface.copy(LocalTransparencyScheme.current.surface)
) {
content()
}

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.ktx.withCorners
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
import kotlin.math.ceil
fun <T : SavableSearchable> LazyListScope.GridResults(
@ -39,7 +39,7 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
bottom = if (!reverse && isBottom) 8.dp else 0.dp,
)
.background(
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
MaterialTheme.colorScheme.surface.copy(alpha = LocalTransparencyScheme.current.surface),
MaterialTheme.shapes.medium.withCorners(
topStart = isTop,
topEnd = isTop,
@ -72,7 +72,7 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
bottom = if (!reverse && isLast) 8.dp else 0.dp,
)
.background(
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
MaterialTheme.colorScheme.surface.copy(alpha = LocalTransparencyScheme.current.surface),
MaterialTheme.shapes.medium.withCorners(
topStart = isFirst && !reverse || isLast && reverse,
topEnd = isFirst && !reverse || isLast && reverse,
@ -120,7 +120,7 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
bottom = if (!reverse) 8.dp else 0.dp,
)
.background(
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
MaterialTheme.colorScheme.surface.copy(alpha = LocalTransparencyScheme.current.surface),
MaterialTheme.shapes.medium.withCorners(
topStart = isTop,
topEnd = isTop,

View File

@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.ktx.animateCorners
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
fun <T : SavableSearchable> LazyListScope.ListResults(
key: String,
@ -104,7 +104,7 @@ fun LazyItemScope.ListItemSurface(
if (it) 2.dp else 0.dp
}
val backgroundAlpha by transition.animateFloat {
if (it) 1f else LocalCardStyle.current.opacity
if (it) 1f else LocalTransparencyScheme.current.surface
}
val padding by transition.animateDp {

View File

@ -22,7 +22,7 @@ import de.mm20.launcher2.ui.common.FavoritesTagSelector
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
fun LazyListScope.SearchFavorites(
favorites: List<SavableSearchable>,
@ -47,7 +47,7 @@ fun LazyListScope.SearchFavorites(
)
.background(
MaterialTheme.colorScheme.surface.copy(
LocalCardStyle.current.opacity
LocalTransparencyScheme.current.surface
),
MaterialTheme.shapes.medium
)

View File

@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
@Composable
fun KeyboardFilterBar(
@ -43,6 +44,7 @@ fun KeyboardFilterBar(
HorizontalDivider()
Row(
modifier = Modifier
.consumeAllScrolling()
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,

View File

@ -18,10 +18,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Badge
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@ -52,11 +49,10 @@ fun LauncherSearchBar(
modifier: Modifier = Modifier,
style: SearchBarStyle,
level: () -> SearchBarLevel,
value: () -> String,
focused: Boolean,
onFocusChange: (Boolean) -> Unit,
actions: List<SearchAction>,
highlightedAction: SearchAction?,
highlightedAction: SearchAction? = null,
isSearchOpen: Boolean = false,
darkColors: Boolean = false,
bottomSearchBar: Boolean = false,
@ -77,18 +73,15 @@ fun LauncherSearchBar(
else focusManager.clearFocus()
}
val filterBar by searchVM.filterBar.collectAsState(false)
val _value = value()
val value by searchVM.searchQuery
Box(modifier = modifier) {
SearchBar(
modifier = Modifier
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(8.dp)
.offset { IntOffset(0, searchBarOffset()) },
style = style, level = level(), value = _value, onValueChange = {
style = style, level = level(), value = value, onValueChange = {
searchVM.search(it)
},
reverse = bottomSearchBar,
@ -136,7 +129,7 @@ fun LauncherSearchBar(
}
}
}
SearchBarMenu(searchBarValue = _value, onInputClear = {
SearchBarMenu(searchBarValue = value, onInputClear = {
searchVM.reset()
})
},
@ -152,22 +145,5 @@ fun LauncherSearchBar(
onUnfocus = { onFocusChange(false) },
onKeyboardActionGo = onKeyboardActionGo
)
AnimatedVisibility (filterBar && isSearchOpen && !searchVM.showFilters.value
// Use imeAnimationTarget instead of isImeVisible to animate the filter bar at the same time as the keyboard
&& WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter)
) {
val items by searchVM.filterBarItems.collectAsState(emptyList())
KeyboardFilterBar(
filters = searchVM.filters.value,
onFiltersChange = {
searchVM.setFilters(it)
},
items = items
)
}
}
}

View File

@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchActionIcon
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
import de.mm20.launcher2.ui.settings.SettingsActivity
@Composable
@ -37,6 +38,7 @@ fun ColumnScope.SearchBarActions(
AnimatedVisibility(actions.isNotEmpty()) {
LazyRow(
modifier = Modifier
.consumeAllScrolling()
.height(48.dp)
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp),
verticalAlignment = Alignment.CenterVertically,

View File

@ -77,22 +77,6 @@ fun RowScope.SearchBarMenu(
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
}
)
val editButton by widgetsVM.editButton.collectAsState()
val searchOpen by launcherVM.isSearchOpen
if (!searchOpen && editButton == false) {
DropdownMenuItem(
onClick = {
launcherVM.setWidgetEditMode(editMode = true)
showOverflowMenu = false
},
text = {
Text(stringResource(R.string.menu_edit_widgets))
},
leadingIcon = {
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
}
)
}
DropdownMenuItem(
onClick = {
context.startActivity(Intent(context, SettingsActivity::class.java))

View File

@ -18,8 +18,9 @@ import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.gestures.Gesture
import de.mm20.launcher2.ui.launcher.FailedGesture
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
data class FailedGesture(val gesture: Gesture, val action: GestureAction)
@Composable
fun FailedGestureSheet(
@ -43,7 +44,9 @@ fun FailedGestureSheet(
Gesture.SwipeDown -> R.string.preference_gesture_swipe_down
Gesture.SwipeLeft -> R.string.preference_gesture_swipe_left
Gesture.SwipeRight -> R.string.preference_gesture_swipe_right
Gesture.HomeButton -> R.string.preference_gesture_home_button
Gesture.SwipeUp -> R.string.preference_gesture_swipe_up
else -> throw IllegalArgumentException("Unknown gesture: ${failedGesture.gesture}")
//Gesture.HomeButton -> R.string.preference_gesture_home_button
})
BottomSheetDialog(

View File

@ -2,13 +2,11 @@ package de.mm20.launcher2.ui.launcher.sheets
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.ui.GestureSettings
import de.mm20.launcher2.ui.gestures.Gesture
import kotlinx.coroutines.launch
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -27,7 +25,8 @@ class FailedGestureSheetVM : ViewModel(), KoinComponent {
Gesture.SwipeDown -> gestureSettings.setSwipeDown(GestureAction.NoAction)
Gesture.SwipeLeft -> gestureSettings.setSwipeLeft(GestureAction.NoAction)
Gesture.SwipeRight -> gestureSettings.setSwipeRight(GestureAction.NoAction)
Gesture.HomeButton -> gestureSettings.setHomeButton(GestureAction.NoAction)
//Gesture.HomeButton -> gestureSettings.setHomeButton(GestureAction.NoAction)
else -> {}
}
}
}

View File

@ -8,7 +8,9 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
SavedStateRegistry.SavedStateProvider {
@ -16,6 +18,7 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
val editFavoritesSheetShown = mutableStateOf(false)
val hiddenItemsSheetShown = mutableStateOf(false)
val editTagSheetShown = mutableStateOf<String?>(null)
val failedGestureSheetShown = mutableStateOf<FailedGesture?>(null)
init {
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
@ -73,6 +76,13 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
editTagSheetShown.value = null
}
fun showFailedGestureSheet(gesture: Gesture, action: GestureAction) {
failedGestureSheetShown.value = FailedGesture(gesture, action)
}
fun dismissFailedGestureSheet() {
failedGestureSheetShown.value = null
}
companion object {
private const val PROVIDER = "bottom_sheet_manager"
private const val FAVORITES = "favorites"

View File

@ -16,4 +16,7 @@ fun LauncherBottomSheets() {
bottomSheetManager.editTagSheetShown.value?.let {
EditTagSheet(tag = it, onDismiss = { bottomSheetManager.dismissEditTagSheet() })
}
bottomSheetManager.failedGestureSheetShown.value?.let {
FailedGestureSheet(it, onDismiss = { bottomSheetManager.dismissFailedGestureSheet() })
}
}

View File

@ -8,7 +8,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
@ -114,7 +118,7 @@ fun WidgetColumn(
swapThresholds[i][0] = it.positionInParent().y
swapThresholds[i][1] = it.positionInParent().y + it.size.height
}
.padding(top = 8.dp)
.padding(top = if (i > 0) 8.dp else 0.dp)
.offset {
IntOffset(0, offsetY.value.toInt())
},
@ -156,31 +160,28 @@ fun WidgetColumn(
)
val icon =
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
ExtendedFloatingActionButton(
modifier = Modifier
.semantics {
role = Role.Button
contentDescription = title
}
.padding(16.dp)
.align(Alignment.CenterHorizontally),
icon = {
Icon(
painter = rememberAnimatedVectorPainter(
animatedImageVector = icon,
atEnd = !editMode
), contentDescription = null
)
},
text = {
Text(title)
}, onClick = {
Button(
modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 8.dp),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
onClick = {
if (!editMode) {
onEditModeChange(true)
} else {
addNewWidget = true
}
})
}
) {
Icon(
modifier = Modifier.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
painter = rememberAnimatedVectorPainter(
animatedImageVector = icon,
atEnd = !editMode
), contentDescription = null
)
Text(title)
}
}
}

View File

@ -10,18 +10,14 @@ import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.DragIndicator
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -37,17 +33,15 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.sheets.ConfigureWidgetSheet
import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget
import de.mm20.launcher2.ui.launcher.widgets.external.AppWidget
import de.mm20.launcher2.ui.launcher.widgets.favorites.FavoritesWidget
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.notes.NotesWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.FavoritesWidget
@ -72,14 +66,14 @@ fun WidgetItem(
var configure by rememberSaveable { mutableStateOf(false) }
var isDragged by remember { mutableStateOf(false) }
val elevation by animateDpAsState(if (isDragged) 8.dp else 2.dp)
val elevation by animateDpAsState(if (isDragged) 8.dp else 0.dp)
val appWidget = if (widget is AppWidget) remember(widget.config.widgetId) {
AppWidgetManager.getInstance(context).getAppWidgetInfo(widget.config.widgetId)
} else null
val backgroundOpacity by animateFloatAsState(
if (widget is AppWidget && !widget.config.background && !editMode) 0f else LocalCardStyle.current.opacity,
if (widget is AppWidget && !widget.config.background && !editMode) 0f else LocalTransparencyScheme.current.surface,
label = "widgetCardBackgroundOpacity",
)

View File

@ -27,7 +27,6 @@ import androidx.compose.material.icons.rounded.AutoAwesome
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.ColorLens
import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.Height
import androidx.compose.material.icons.rounded.HorizontalSplit
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.MusicNote
@ -563,21 +562,13 @@ fun ConfigureClockWidgetSheet(
}
}
}
if (fillHeight == true) {
OutlinedCard(
modifier = Modifier.padding(top = 16.dp),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
SwitchPreference(
title = stringResource(R.string.preference_clock_widget_fill_height),
icon = Icons.Rounded.Height,
value = fillHeight == true,
onValueChanged = {
viewModel.setFillHeight(it)
}
)
AnimatedVisibility(fillHeight == true) {
var showDropdown by remember { mutableStateOf(false) }
Preference(
title = stringResource(R.string.preference_clock_widget_alignment),

View File

@ -86,8 +86,8 @@ import de.mm20.launcher2.ui.component.Tooltip
import de.mm20.launcher2.ui.ktx.conditional
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionParams
import de.mm20.launcher2.ui.launcher.transitions.HandleEnterHomeTransition
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
import de.mm20.launcher2.widgets.MusicWidget
import kotlin.math.min
@ -352,7 +352,7 @@ fun MusicWidget(widget: MusicWidget) {
) {
FilledTonalIconButton(
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalCardStyle.current.opacity),
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalTransparencyScheme.current.surface),
),
onClick = { viewModel.togglePause() },
) {

View File

@ -5,13 +5,11 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.format.DateUtils
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -81,8 +79,7 @@ import de.mm20.launcher2.ui.component.Tooltip
import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon
import de.mm20.launcher2.ui.component.weather.WeatherIcon
import de.mm20.launcher2.ui.ktx.blendIntoViewScale
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
import de.mm20.launcher2.weather.DailyForecast
import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.widgets.WeatherWidget
@ -121,7 +118,9 @@ fun WeatherWidget(widget: WeatherWidget) {
Column {
if (!isProviderAvailable) {
Banner(
modifier = Modifier.fillMaxWidth().padding(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResource(R.string.weather_widget_no_provider),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
@ -134,7 +133,9 @@ fun WeatherWidget(widget: WeatherWidget) {
Icon(
Icons.AutoMirrored.Rounded.OpenInNew,
null,
modifier = Modifier.padding(end = ButtonDefaults.IconSpacing).size(ButtonDefaults.IconSize)
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.settings))
}
@ -179,7 +180,7 @@ fun WeatherWidget(widget: WeatherWidget) {
val currentDayForecasts by viewModel.currentDayForecasts
Surface(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = LocalCardStyle.current.opacity),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = LocalTransparencyScheme.current.surface),
modifier = Modifier.fillMaxWidth()
) {
Column(
@ -296,7 +297,7 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
topEnd = CornerSize(0),
bottomEnd = CornerSize(0)
),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalCardStyle.current.opacity),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalTransparencyScheme.current.surface),
) {
Text(
text = "${forecast.provider} (${
@ -451,8 +452,7 @@ fun WeatherTimeSelector(
LazyRow(
state = listState,
modifier = modifier
.fillMaxWidth()
.consumeAllScrolling(),
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically
@ -476,7 +476,8 @@ fun WeatherTimeSelector(
verticalArrangement = Arrangement.SpaceEvenly
) {
WeatherIcon(
modifier = Modifier.align(Alignment.CenterHorizontally)
modifier = Modifier
.align(Alignment.CenterHorizontally)
.semantics {
contentDescription = fc.condition
},
@ -515,8 +516,7 @@ fun WeatherDaySelector(
LazyRow(
state = listState,
modifier = modifier
.fillMaxWidth()
.consumeAllScrolling(),
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(start = 16.dp, end = 16.dp),

View File

@ -9,7 +9,6 @@ import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -71,9 +70,6 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
val fillHeight = settings.fillHeight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFillHeight(fillHeight: Boolean) {
settings.setFillHeight(fillHeight)
}
val parts = settings.parts
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)

View File

@ -6,6 +6,7 @@ import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -17,10 +18,12 @@ import androidx.compose.ui.res.stringResource
import androidx.core.content.FileProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.BuildConfig
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay

View File

@ -3,7 +3,10 @@ package de.mm20.launcher2.ui.settings.debug
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.searchable.SavableSearchableRepository
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -12,6 +15,9 @@ class DebugSettingsScreenVM: ViewModel(), KoinComponent {
private val searchableRepository: SavableSearchableRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()
private val iconService: IconService by inject()
private val uiSettings: UiSettings by inject()
suspend fun cleanUpDatabase(): Int {
var removed = searchableRepository.cleanupDatabase()
removed += customAttributesRepository.cleanupDatabase()

View File

@ -9,8 +9,15 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Adjust
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.SwipeDownAlt
import androidx.compose.material.icons.rounded.SwipeLeftAlt
import androidx.compose.material.icons.rounded.SwipeRightAlt
import androidx.compose.material.icons.rounded.SwipeUpAlt
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -20,6 +27,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -27,7 +35,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R
@ -43,7 +50,6 @@ import de.mm20.launcher2.ui.ktx.toPixels
fun GestureSettingsScreen() {
val viewModel: GestureSettingsScreenVM = viewModel()
val layout by viewModel.baseLayout.collectAsStateWithLifecycle(null)
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
val options = buildList {
@ -54,30 +60,109 @@ fun GestureSettingsScreen() {
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents)
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search)
add(stringResource(R.string.gesture_action_widgets) to GestureAction.Widgets)
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
}
val context = LocalContext.current
PreferenceScreen(title = stringResource(R.string.preference_screen_gestures)) {
item {
PreferenceCategory {
val baseLayout by viewModel.baseLayout.collectAsStateWithLifecycle(null)
ListPreference(title = stringResource(R.string.preference_layout_open_search),
items = listOf(
stringResource(R.string.open_search_pull_down) to BaseLayout.PullDown,
stringResource(R.string.open_search_swipe_left) to BaseLayout.Pager,
stringResource(R.string.open_search_swipe_right) to BaseLayout.PagerReversed,
),
value = baseLayout,
onValueChanged = {
if (it != null) viewModel.setBaseLayout(it)
},
)
}
}
item {
val appIconSize = 32.dp.toPixels()
PreferenceCategory {
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeDown)) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeDownApp by viewModel.swipeDownApp.collectAsState(null)
val swipeDownAppIcon by remember(swipeDownApp?.key) {
viewModel.getIcon(swipeDownApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_down),
icon = Icons.Rounded.SwipeDownAlt,
value = swipeDown,
onValueChanged = { viewModel.setSwipeDown(it) },
options = options,
app = swipeDownApp,
appIcon = swipeDownAppIcon,
onAppChanged = { viewModel.setSwipeDownApp(it) }
)
val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeLeft)) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeLeftApp by viewModel.swipeLeftApp.collectAsState(null)
val swipeLeftAppIcon by remember(swipeLeftApp?.key) {
viewModel.getIcon(swipeLeftApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_left),
icon = Icons.Rounded.SwipeLeftAlt,
value = swipeLeft,
onValueChanged = { viewModel.setSwipeLeft(it) },
options = options,
app = swipeLeftApp,
appIcon = swipeLeftAppIcon,
onAppChanged = { viewModel.setSwipeLeftApp(it) }
)
val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeRight)) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeRightApp by viewModel.swipeRightApp.collectAsState(null)
val swipeRightAppIcon by remember(swipeRightApp?.key) {
viewModel.getIcon(swipeRightApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_right),
icon = Icons.Rounded.SwipeRightAlt,
value = swipeRight,
onValueChanged = { viewModel.setSwipeRight(it) },
options = options,
app = swipeRightApp,
appIcon = swipeRightAppIcon,
onAppChanged = { viewModel.setSwipeRightApp(it) }
)
val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeUp)) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeUpApp by viewModel.swipeUpApp.collectAsState(null)
val swipeUpAppIcon by remember(swipeUpApp?.key) {
viewModel.getIcon(swipeUpApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_up),
icon = Icons.Rounded.SwipeUpAlt,
value = swipeUp,
onValueChanged = { viewModel.setSwipeUp(it) },
options = options,
app = swipeUpApp,
appIcon = swipeUpAppIcon,
onAppChanged = { viewModel.setSwipeUpApp(it) }
)
val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(doubleTap)) {
MissingPermissionBanner(
@ -92,9 +177,9 @@ fun GestureSettingsScreen() {
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_double_tap),
icon = Icons.Rounded.Adjust,
value = doubleTap,
onValueChanged = { viewModel.setDoubleTap(it) },
isOpenSearch = false,
options = options,
app = doubleTapApp,
appIcon = doubleTapAppIcon,
@ -115,87 +200,15 @@ fun GestureSettingsScreen() {
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_long_press),
icon = Icons.Rounded.Circle,
value = longPress,
onValueChanged = { viewModel.setLongPress(it) },
isOpenSearch = false,
options = options,
app = longPressApp,
appIcon = longPressAppIcon,
onAppChanged = { viewModel.setLongPressApp(it) }
)
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null)
val swipeDownIsSearch = layout == BaseLayout.PullDown
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeDown) && !swipeDownIsSearch) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeDownApp by viewModel.swipeDownApp.collectAsState(null)
val swipeDownAppIcon by remember(swipeDownApp?.key) {
viewModel.getIcon(swipeDownApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_down),
value = swipeDown,
onValueChanged = { viewModel.setSwipeDown(it) },
isOpenSearch = swipeDownIsSearch,
options = options,
app = swipeDownApp,
appIcon = swipeDownAppIcon,
onAppChanged = { viewModel.setSwipeDownApp(it) }
)
val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null)
val swipeLeftIsSearch = layout == BaseLayout.Pager
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeLeft) && !swipeLeftIsSearch) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeLeftApp by viewModel.swipeLeftApp.collectAsState(null)
val swipeLeftAppIcon by remember(swipeLeftApp?.key) {
viewModel.getIcon(swipeLeftApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_left),
value = swipeLeft,
onValueChanged = { viewModel.setSwipeLeft(it) },
isOpenSearch = swipeLeftIsSearch,
options = options,
app = swipeLeftApp,
appIcon = swipeLeftAppIcon,
onAppChanged = { viewModel.setSwipeLeftApp(it) }
)
val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null)
val swipeRightIsSearch = layout == BaseLayout.PagerReversed
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeRight) && !swipeRightIsSearch) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
)
}
val swipeRightApp by viewModel.swipeRightApp.collectAsState(null)
val swipeRightAppIcon by remember(swipeRightApp?.key) {
viewModel.getIcon(swipeRightApp, appIconSize.toInt())
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_swipe_right),
value = swipeRight,
onValueChanged = { viewModel.setSwipeRight(it) },
isOpenSearch = swipeRightIsSearch,
options = options,
app = swipeRightApp,
appIcon = swipeRightAppIcon,
onAppChanged = { viewModel.setSwipeRightApp(it) }
)
val homeButton by viewModel.homeButton.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(homeButton)) {
MissingPermissionBanner(
@ -210,9 +223,9 @@ fun GestureSettingsScreen() {
}.collectAsState(null)
GesturePreference(
title = stringResource(R.string.preference_gesture_home_button),
icon = Icons.Rounded.Home,
value = homeButton,
onValueChanged = { viewModel.setHomeButton(it) },
isOpenSearch = false,
options = options,
app = homeButtonApp,
appIcon = homeButtonAppIcon,
@ -230,6 +243,7 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
is GestureAction.QuickSettings,
is GestureAction.Recents,
is GestureAction.PowerMenu -> true
else -> false
}
}
@ -237,9 +251,9 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
@Composable
fun GesturePreference(
title: String,
icon: ImageVector,
value: GestureAction?,
onValueChanged: (GestureAction) -> Unit,
isOpenSearch: Boolean,
options: List<Pair<String, GestureAction>>,
app: SavableSearchable?,
appIcon: LauncherIcon?,
@ -254,14 +268,15 @@ fun GesturePreference(
) {
ListPreference(
title = title,
enabled = !isOpenSearch,
icon = icon,
items = options,
value = if (isOpenSearch) GestureAction.Search else value,
value = value,
summary = options.find { value?.javaClass == it.second.javaClass }?.first,
onValueChanged = { if (it != null) onValueChanged(it) }
)
}
if (value is GestureAction.Launch && !isOpenSearch) {
if (value is GestureAction.Launch) {
Box(
modifier = Modifier
.height(36.dp)
@ -269,7 +284,8 @@ fun GesturePreference(
.alpha(0.38f)
.background(LocalContentColor.current)
)
Box(modifier = Modifier
Box(
modifier = Modifier
.clickable { showAppPicker = true }
.padding(12.dp)) {
ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
@ -277,7 +293,7 @@ fun GesturePreference(
}
}
if (!isOpenSearch && value is GestureAction.Launch && (showAppPicker || app == null)) {
if (value is GestureAction.Launch && (showAppPicker || app == null)) {
SearchablePicker(
onDismissRequest = {
showAppPicker = false

View File

@ -34,20 +34,14 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val baseLayout = uiSettings.baseLayout
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setBaseLayout(baseLayout: BaseLayout) {
uiSettings.setBaseLayout(baseLayout)
}
val swipeDown = gestureSettings.swipeDown
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val swipeLeft = gestureSettings.swipeLeft
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val swipeRight = gestureSettings.swipeRight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val swipeUp = gestureSettings.swipeUp
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val doubleTap = gestureSettings.doubleTap
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val longPress = gestureSettings.longPress
@ -67,6 +61,10 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
gestureSettings.setSwipeRight(action)
}
fun setSwipeUp(action: GestureAction) {
gestureSettings.setSwipeUp(action)
}
fun setDoubleTap(action: GestureAction) {
gestureSettings.setDoubleTap(action)
}
@ -121,6 +119,20 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
setSwipeDown(GestureAction.Launch(searchable.key))
}
val swipeUpApp: Flow<SavableSearchable?> = swipeUp
.flatMapLatest {
if (it !is GestureAction.Launch || it.key == null) flowOf(null)
else searchableRepository.getByKeys(listOf(it.key!!)).map {
it.firstOrNull()
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeUpApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeUp(GestureAction.Launch(searchable.key))
}
val longPressApp: Flow<SavableSearchable?> = longPress
.flatMapLatest {
if (it !is GestureAction.Launch || it.key == null) flowOf(null)

View File

@ -54,6 +54,7 @@ fun HomescreenSettingsScreen() {
val dock by viewModel.dock.collectAsStateWithLifecycle(null)
val fixedRotation by viewModel.fixedRotation.collectAsStateWithLifecycle(null)
val widgetsOnHomeScreen by viewModel.widgetsOnHomeScreen.collectAsStateWithLifecycle(null)
val editButton by viewModel.widgetEditButton.collectAsStateWithLifecycle(null)
val searchBarStyle by viewModel.searchBarStyle.collectAsStateWithLifecycle(null)
val searchBarColor by viewModel.searchBarColor.collectAsStateWithLifecycle(null)
@ -81,18 +82,6 @@ fun HomescreenSettingsScreen() {
)
}
}
item {
PreferenceCategory(stringResource(R.string.preference_clockwidget_favorites_part)) {
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_favorites_part),
summary = stringResource(R.string.preference_clockwidget_favorites_part_summary),
value = dock == true,
onValueChanged = {
viewModel.setDock(it)
},
)
}
}
item {
PreferenceCategory(
title = stringResource(id = R.string.preference_category_widgets)
@ -104,6 +93,21 @@ fun HomescreenSettingsScreen() {
viewModel.showClockWidgetSheet = true
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_favorites_part),
summary = stringResource(R.string.preference_clockwidget_favorites_part_summary),
value = dock == true,
onValueChanged = {
viewModel.setDock(it)
},
)
SwitchPreference(
title = stringResource(R.string.preference_widgets_on_home_screen),
summary = stringResource(R.string.preference_widgets_on_home_screen_summary),
value = widgetsOnHomeScreen == true,
onValueChanged = {
viewModel.setWidgetsOnHomeScreen(it)
})
SwitchPreference(
title = stringResource(id = R.string.preference_edit_button),
summary = stringResource(id = R.string.preference_widgets_edit_button_summary),

View File

@ -13,13 +13,16 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.ScreenOrientation
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.preferences.ui.GestureSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -29,6 +32,7 @@ import org.koin.core.component.get
class HomescreenSettingsScreenVM(
private val uiSettings: UiSettings,
private val clockWidgetSettings: ClockWidgetSettings,
private val gestureSettings: GestureSettings,
) : ViewModel() {
var showClockWidgetSheet by mutableStateOf(false)
@ -120,11 +124,11 @@ class HomescreenSettingsScreenVM(
uiSettings.setBottomSearchBar(bottomSearchBar)
}
val dock = clockWidgetSettings.dock
val dock = uiSettings.dock
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setDock(dock: Boolean) {
clockWidgetSettings.setDock(dock)
uiSettings.setDock(dock)
}
val fixedRotation = uiSettings.orientation.map { it != ScreenOrientation.Auto }
@ -148,12 +152,52 @@ class HomescreenSettingsScreenVM(
uiSettings.setChargingAnimation(chargingAnimation)
}
val widgetsOnHomeScreen = uiSettings.homeScreenWidgets
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setWidgetsOnHomeScreen(widgetsOnHomeScreen: Boolean) {
uiSettings.setHomeScreenWidgets(widgetsOnHomeScreen)
viewModelScope.launch {
val gestures = gestureSettings.first()
if (widgetsOnHomeScreen) {
if (gestures.swipeUp is GestureAction.Widgets) {
gestureSettings.setSwipeUp(GestureAction.NoAction)
} else if (gestures.swipeRight is GestureAction.Widgets) {
gestureSettings.setSwipeUp(GestureAction.NoAction)
} else if (gestures.swipeLeft is GestureAction.Widgets) {
gestureSettings.setSwipeUp(GestureAction.NoAction)
} else if (gestures.swipeDown is GestureAction.Widgets) {
gestureSettings.setSwipeUp(GestureAction.NoAction)
} else if (gestures.longPress is GestureAction.Widgets) {
gestureSettings.setLongPress(GestureAction.NoAction)
} else if (gestures.doubleTap is GestureAction.Widgets) {
gestureSettings.setDoubleTap(GestureAction.NoAction)
}
} else {
if (gestures.swipeUp is GestureAction.NoAction || gestures.swipeUp is GestureAction.Widgets) {
gestureSettings.setSwipeUp(GestureAction.Widgets)
} else if (gestures.swipeRight is GestureAction.NoAction || gestures.swipeRight is GestureAction.Widgets) {
gestureSettings.setSwipeRight(GestureAction.Widgets)
} else if (gestures.swipeLeft is GestureAction.NoAction || gestures.swipeLeft is GestureAction.Widgets) {
gestureSettings.setSwipeLeft(GestureAction.Widgets)
} else if (gestures.swipeDown is GestureAction.NoAction || gestures.swipeDown is GestureAction.Widgets) {
gestureSettings.setSwipeDown(GestureAction.Widgets)
} else if (gestures.longPress is GestureAction.NoAction || gestures.longPress is GestureAction.Widgets) {
gestureSettings.setLongPress(GestureAction.Widgets)
} else if (gestures.doubleTap is GestureAction.NoAction || gestures.doubleTap is GestureAction.Widgets) {
gestureSettings.setDoubleTap(GestureAction.Widgets)
}
}
}
}
companion object : KoinComponent {
val Factory = viewModelFactory {
initializer {
HomescreenSettingsScreenVM(
uiSettings = get(),
clockWidgetSettings = get(),
gestureSettings = get(),
)
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.Science
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.SettingsBackupRestore
import androidx.compose.runtime.Composable

View File

@ -48,8 +48,8 @@ fun wallpaperColorsAsState(): State<WallpaperColors> {
if (isAtLeastApiLevel(27)) {
DisposableEffect(null) {
val wallpaperManager = WallpaperManager.getInstance(context)
val callback = callback@{ colors: android.app.WallpaperColors?, which: Int ->
if (which and WallpaperManager.FLAG_SYSTEM == 0) return@callback
val callback = WallpaperManager.OnColorsChangedListener { colors, which ->
if (which and WallpaperManager.FLAG_SYSTEM == 0) return@OnColorsChangedListener
if (colors != null) {
state.value = WallpaperColors.fromPlatformType(colors)
} else {

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.ui.theme.transparency
import androidx.compose.runtime.compositionLocalOf
data class TransparencyScheme(
val background: Float,
val surface: Float,
)
val LocalTransparencyScheme = compositionLocalOf { TransparencyScheme(0.85f, 1f) }

View File

@ -19,6 +19,14 @@
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="AssistantTheme" parent="LauncherTheme">
<item name="android:windowShowWallpaper">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowActivityTransitions">true</item>
<item name="android:windowEnterTransition">@android:transition/fade</item>
<item name="android:windowExitTransition">@android:transition/fade</item>
</style>
<style name="DialogTheme" parent="BaseTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>

View File

@ -668,6 +668,8 @@
<string name="preference_compact_tags">Compact tags</string>
<string name="preference_compact_tags_summary">Hide tag labels or icons to reduce the space occupied by tags</string>
<string name="preference_widgets_edit_button_summary">Show a button to add, remove and rearrange widgets</string>
<string name="preference_widgets_on_home_screen">Widgets on home screen</string>
<string name="preference_widgets_on_home_screen_summary">Show widgets on the home screen instead of an extra page</string>
<string name="preference_screen_homescreen">Home screen</string>
<string name="preference_screen_homescreen_summary">Clock, search bar, wallpaper, system bars</string>
<string name="preference_screen_icons">Grid &amp; icons</string>
@ -772,11 +774,13 @@
<string name="preference_gesture_swipe_down">Swipe down</string>
<string name="preference_gesture_swipe_left">Swipe left</string>
<string name="preference_gesture_swipe_right">Swipe right</string>
<string name="preference_gesture_swipe_up">Swipe up</string>
<string name="preference_gesture_double_tap">Double tap</string>
<string name="preference_gesture_long_press">Long press</string>
<string name="preference_gesture_home_button">Home button/gesture</string>
<string name="gesture_action_none">Do nothing</string>
<string name="gesture_action_open_search">Open search</string>
<string name="gesture_action_widgets">Widgets</string>
<string name="gesture_action_launch_app">Launch app</string>
<string name="gesture_action_notifications">Open notification drawer</string>
<string name="gesture_action_lock_screen">Turn off screen</string>
@ -1023,4 +1027,6 @@
<item quantity="other">in %1$d minutes</item>
</plurals>
<string name="departure_time_departed">departed</string>
<string name="bad_configuration_title">Congratulations, you\'ve locked yourself out!</string>
<string name="bad_configuration_summary">You\'ve discovered a combination of settings that makes both the search and settings inaccessible — effectively locking you out of the launcher.</string>
</resources>

View File

@ -4,6 +4,7 @@ import android.content.Context
import de.mm20.launcher2.preferences.migrations.Migration2
import de.mm20.launcher2.preferences.migrations.Migration3
import de.mm20.launcher2.preferences.migrations.Migration4
import de.mm20.launcher2.preferences.migrations.Migration5
import de.mm20.launcher2.settings.BaseSettings
internal class LauncherDataStore(
@ -16,6 +17,7 @@ internal class LauncherDataStore(
Migration2(),
Migration3(),
Migration4(),
Migration5(),
),
) {

View File

@ -4,15 +4,17 @@ import android.content.Context
import de.mm20.launcher2.search.SearchFilters
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
data class LauncherSettingsData internal constructor(
val schemaVersion: Int = 3,
val schemaVersion: Int = 5,
val uiColorScheme: ColorScheme = ColorScheme.System,
val uiTheme: ThemeDescriptor = ThemeDescriptor.Default,
val uiCompatModeColors: Boolean = false,
val uiFont: Font = Font.Outfit,
@Deprecated("No longer in use, only used for migration")
val uiBaseLayout: BaseLayout = BaseLayout.PullDown,
val uiOrientation: ScreenOrientation = ScreenOrientation.Auto,
@ -39,10 +41,12 @@ data class LauncherSettingsData internal constructor(
val clockWidgetBatteryPart: Boolean = true,
val clockWidgetMusicPart: Boolean = true,
val clockWidgetDatePart: Boolean = true,
@Deprecated("Use homeScreenWidgets")
val clockWidgetFillHeight: Boolean = true,
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,
val homeScreenDock: Boolean = false,
val homeScreenWidgets: Boolean = false,
val favoritesEnabled: Boolean = true,
val favoritesFrequentlyUsed: Boolean = true,
@ -123,9 +127,10 @@ data class LauncherSettingsData internal constructor(
val widgetsEditButton: Boolean = true,
val gesturesSwipeDown: GestureAction = GestureAction.Notifications,
val gesturesSwipeDown: GestureAction = GestureAction.Search,
val gesturesSwipeLeft: GestureAction = GestureAction.NoAction,
val gesturesSwipeRight: GestureAction = GestureAction.NoAction,
val gesturesSwipeUp: GestureAction = GestureAction.Widgets,
val gesturesDoubleTap: GestureAction = GestureAction.ScreenLock,
val gesturesLongPress: GestureAction = GestureAction.NoAction,
val gesturesHomeButton: GestureAction = GestureAction.NoAction,
@ -366,6 +371,10 @@ sealed interface GestureAction {
@SerialName("search")
data object Search : GestureAction
@Serializable
@SerialName("widgets")
data object Widgets : GestureAction
@Serializable
@SerialName("power_menu")
data object PowerMenu : GestureAction

View File

@ -0,0 +1,26 @@
package de.mm20.launcher2.preferences.migrations
import androidx.datastore.core.DataMigration
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.LauncherSettingsData
class Migration5 : DataMigration<LauncherSettingsData> {
override suspend fun cleanUp() {
}
override suspend fun migrate(currentData: LauncherSettingsData): LauncherSettingsData {
return currentData.copy(
schemaVersion = 5,
gesturesSwipeDown = if (currentData.uiBaseLayout == BaseLayout.PullDown) GestureAction.Search else currentData.gesturesSwipeDown,
gesturesSwipeLeft = if (currentData.uiBaseLayout == BaseLayout.Pager) GestureAction.Search else currentData.gesturesSwipeLeft,
gesturesSwipeRight = if (currentData.uiBaseLayout == BaseLayout.PagerReversed) GestureAction.Search else currentData.gesturesSwipeRight,
gesturesSwipeUp = GestureAction.Widgets,
homeScreenWidgets = !currentData.clockWidgetFillHeight,
)
}
override suspend fun shouldMigrate(currentData: LauncherSettingsData): Boolean {
return currentData.schemaVersion < 5
}
}

View File

@ -64,23 +64,11 @@ class ClockWidgetSettings internal constructor(
}
val fillHeight
get() = launcherDataStore.data.map { it.clockWidgetFillHeight }
fun setFillHeight(fillHeight: Boolean) {
launcherDataStore.update {
it.copy(clockWidgetFillHeight = fillHeight)
}
}
get() = launcherDataStore.data.map { !it.homeScreenWidgets }
val dock
get() = launcherDataStore.data.map { it.homeScreenDock }
fun setDock(dock: Boolean) {
launcherDataStore.update {
it.copy(homeScreenDock = dock)
}
}
val alignment
get() = launcherDataStore.data.map { it.clockWidgetAlignment }

View File

@ -10,6 +10,7 @@ data class GestureSettingsData(
val swipeDown: GestureAction,
val swipeLeft: GestureAction,
val swipeRight: GestureAction,
val swipeUp: GestureAction,
val doubleTap: GestureAction,
val longPress: GestureAction,
val homeButton: GestureAction,
@ -23,6 +24,7 @@ class GestureSettings internal constructor(
swipeDown = it.gesturesSwipeDown,
swipeLeft = it.gesturesSwipeLeft,
swipeRight = it.gesturesSwipeRight,
swipeUp = it.gesturesSwipeUp,
doubleTap = it.gesturesDoubleTap,
longPress = it.gesturesLongPress,
homeButton = it.gesturesHomeButton,
@ -38,6 +40,9 @@ class GestureSettings internal constructor(
val swipeRight: Flow<GestureAction> = dataStore.data.map { it.gesturesSwipeRight }
.distinctUntilChanged()
val swipeUp: Flow<GestureAction> = dataStore.data.map { it.gesturesSwipeUp }
.distinctUntilChanged()
val doubleTap: Flow<GestureAction> = dataStore.data.map { it.gesturesDoubleTap }
.distinctUntilChanged()
@ -65,6 +70,12 @@ class GestureSettings internal constructor(
}
}
fun setSwipeUp(action: GestureAction) {
dataStore.update {
it.copy(gesturesSwipeUp = action)
}
}
fun setDoubleTap(action: GestureAction) {
dataStore.update {
it.copy(gesturesDoubleTap = action)

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.preferences.ui
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.ColorScheme
import de.mm20.launcher2.preferences.Font
import de.mm20.launcher2.preferences.IconShape
@ -225,20 +224,9 @@ class UiSettings internal constructor(
}
}
val baseLayout
get() = launcherDataStore.data.map {
it.uiBaseLayout
}.distinctUntilChanged()
fun setBaseLayout(baseLayout: BaseLayout) {
launcherDataStore.update {
it.copy(uiBaseLayout = baseLayout)
}
}
val clockFillScreen
get() = launcherDataStore.data.map {
it.clockWidgetFillHeight
it.homeScreenWidgets
}.distinctUntilChanged()
val searchBarStyle
@ -347,6 +335,23 @@ class UiSettings internal constructor(
it.homeScreenDock
}.distinctUntilChanged()
fun setDock(dock: Boolean) {
launcherDataStore.update {
it.copy(homeScreenDock = dock)
}
}
val homeScreenWidgets
get() = launcherDataStore.data.map {
it.homeScreenWidgets
}.distinctUntilChanged()
fun setHomeScreenWidgets(widgets: Boolean) {
launcherDataStore.update {
it.copy(homeScreenWidgets = widgets)
}
}
val widgetEditButton
get() = launcherDataStore.data.map {
it.widgetsEditButton

View File

@ -19,8 +19,8 @@ kotlinx-serialization = "1.8.1"
jetbrains-markdown = "0.7.3"
androidx-compose = "1.9.0-alpha02"
androidx-compose-material3 = "1.4.0-alpha14"
androidx-compose = "1.9.0-alpha03"
androidx-compose-material3 = "1.4.0-alpha15"
androidx-compose-materialicons = "1.7.8"
androidx-lifecycle = "2.9.0"
androidx-core = "1.16.0"
@ -37,6 +37,7 @@ androidx-constraint-layout = "1.1.1"
androidx-emojipicker = "1.5.0"
accompanist = "0.36.0"
haze = "1.6.0"
coil = "2.7.0"
koin = "4.0.4"
retrofit = "2.11.0"
@ -95,6 +96,7 @@ accompanist-pager = { group = "com.google.accompanist", name = "accompanist-page
accompanist-pagerindicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanist" }
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }
accompanist-navigationanimation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "accompanist" }
haze = {group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "androidx-constraint-layout" }
androidx-transition = { group = "androidx.transition", name = "transition", version = "1.6.0" }