Layout rewrite please read https://github.com/MM2-0/Kvaesitso/discussions/1431
Rewrite of the underlying layout and gesture handling
This commit is contained in:
parent
fe322fc558
commit
a24d1b8798
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@ -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" />
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -73,7 +73,6 @@ fun FavoritesTagSelector(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.consumeAllScrolling()
|
||||
.horizontalScroll(scrollState)
|
||||
.padding(end = 12.dp),
|
||||
) {
|
||||
|
||||
@ -51,7 +51,6 @@ fun FakeSplashScreen(
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
shadowElevation = 4.dp,
|
||||
color = animatedBackgroundColor,
|
||||
) {
|
||||
Box(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
package de.mm20.launcher2.ui.gestures
|
||||
|
||||
enum class Gesture {
|
||||
DoubleTap,
|
||||
LongPress,
|
||||
SwipeDown,
|
||||
SwipeLeft,
|
||||
SwipeRight,
|
||||
HomeButton,
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
@ -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 {}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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() })
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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() },
|
||||
) {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) }
|
||||
@ -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>
|
||||
|
||||
@ -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 & 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>
|
||||
@ -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(),
|
||||
),
|
||||
) {
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user