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="STATIC_FIELD_NAME_PREFIX" value="s" />
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
<value>
|
<value>
|
||||||
|
<package name="" withSubpackages="true" static="false" module="true" />
|
||||||
<package name="android" withSubpackages="true" static="false" />
|
<package name="android" withSubpackages="true" static="false" />
|
||||||
<emptyLine />
|
<emptyLine />
|
||||||
<package name="com" withSubpackages="true" static="false" />
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
|||||||
@ -51,6 +51,7 @@ android {
|
|||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi",
|
"-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi",
|
||||||
|
"-Xwhen-guards",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +96,8 @@ dependencies {
|
|||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.navigationanimation)
|
implementation(libs.accompanist.navigationanimation)
|
||||||
|
|
||||||
|
implementation(libs.haze)
|
||||||
|
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.activitycompose)
|
implementation(libs.androidx.activitycompose)
|
||||||
implementation(libs.bundles.androidx.lifecycle)
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
android:resumeWhilePausing="true"
|
android:resumeWhilePausing="true"
|
||||||
android:stateNotNeeded="true"
|
android:stateNotNeeded="true"
|
||||||
android:theme="@style/LauncherTheme"
|
android:theme="@style/LauncherTheme"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@ -37,11 +38,13 @@
|
|||||||
android:name=".assistant.AssistantActivity"
|
android:name=".assistant.AssistantActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity="de.mm20.launcher2.assistant"
|
||||||
android:resumeWhilePausing="true"
|
android:resumeWhilePausing="true"
|
||||||
android:stateNotNeeded="true"
|
android:stateNotNeeded="true"
|
||||||
android:theme="@style/LauncherTheme"
|
android:theme="@style/AssistantTheme"
|
||||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
android:windowSoftInputMode="stateHidden|adjustResize"
|
||||||
|
android:enableOnBackInvokedCallback="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.ASSIST" />
|
<action android:name="android.intent.action.ASSIST" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<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.LocalCardStyle
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
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.FavoritesWidget
|
||||||
import de.mm20.launcher2.widgets.WidgetRepository
|
import de.mm20.launcher2.widgets.WidgetRepository
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -46,6 +48,10 @@ fun ProvideSettings(
|
|||||||
LocalCardStyle provides cardStyle,
|
LocalCardStyle provides cardStyle,
|
||||||
LocalFavoritesEnabled provides favoritesEnabled,
|
LocalFavoritesEnabled provides favoritesEnabled,
|
||||||
LocalGridSettings provides gridSettings,
|
LocalGridSettings provides gridSettings,
|
||||||
|
LocalTransparencyScheme provides TransparencyScheme(
|
||||||
|
background = cardStyle.opacity * 0.85f,
|
||||||
|
surface = cardStyle.opacity,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
ProvideIconShape(iconShape) {
|
ProvideIconShape(iconShape) {
|
||||||
content()
|
content()
|
||||||
|
|||||||
@ -73,7 +73,6 @@ fun FavoritesTagSelector(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.consumeAllScrolling()
|
|
||||||
.horizontalScroll(scrollState)
|
.horizontalScroll(scrollState)
|
||||||
.padding(end = 12.dp),
|
.padding(end = 12.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -51,7 +51,6 @@ fun FakeSplashScreen(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
shadowElevation = 4.dp,
|
|
||||||
color = animatedBackgroundColor,
|
color = animatedBackgroundColor,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@ -25,12 +25,13 @@ import androidx.compose.ui.unit.Density
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
|
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LauncherCard(
|
fun LauncherCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
elevation: Dp = 2.dp,
|
elevation: Dp = 2.dp,
|
||||||
backgroundOpacity: Float = LocalCardStyle.current.opacity,
|
backgroundOpacity: Float = LocalTransparencyScheme.current.surface,
|
||||||
shape: Shape = MaterialTheme.shapes.medium,
|
shape: Shape = MaterialTheme.shapes.medium,
|
||||||
color: Color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
color: Color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
||||||
border: BorderStroke? = LocalCardStyle.current.borderWidth.takeIf { it > 0 }
|
border: BorderStroke? = LocalCardStyle.current.borderWidth.takeIf { it > 0 }
|
||||||
@ -55,7 +56,7 @@ fun PartialLauncherCard(
|
|||||||
isTop: Boolean = false,
|
isTop: Boolean = false,
|
||||||
isBottom: Boolean = false,
|
isBottom: Boolean = false,
|
||||||
elevation: Dp = 2.dp,
|
elevation: Dp = 2.dp,
|
||||||
backgroundOpacity: Float = LocalCardStyle.current.opacity,
|
backgroundOpacity: Float = LocalTransparencyScheme.current.surface,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -79,7 +80,7 @@ fun PartialLauncherCard(
|
|||||||
private fun CardMiddlePiece(
|
private fun CardMiddlePiece(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
elevation: Dp,
|
elevation: Dp,
|
||||||
backgroundOpacity: Float = LocalCardStyle.current.opacity,
|
backgroundOpacity: Float = LocalTransparencyScheme.current.surface,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val borderWidth = LocalCardStyle.current.borderWidth.dp
|
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.R
|
||||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
|
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(
|
||||||
@ -102,7 +103,7 @@ fun SearchBar(
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
when {
|
when {
|
||||||
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
|
it == SearchBarLevel.Active -> LocalTransparencyScheme.current.surface
|
||||||
style != SearchBarStyle.Transparent -> 1f
|
style != SearchBarStyle.Transparent -> 1f
|
||||||
it == SearchBarLevel.Resting -> 0f
|
it == SearchBarLevel.Resting -> 0f
|
||||||
else -> 1f
|
else -> 1f
|
||||||
@ -165,9 +166,6 @@ fun SearchBar(
|
|||||||
color = contentColor
|
color = contentColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(level) {
|
|
||||||
if (level == SearchBarLevel.Resting) onUnfocus()
|
|
||||||
}
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.onFocusChanged {
|
.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
|
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
|
||||||
* and search is closed)
|
* 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) {
|
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
val navContract = intent?.let { GestureNavContract.fromIntent(it) }
|
val navContract = intent.let { GestureNavContract.fromIntent(it) }
|
||||||
if (navContract != null) {
|
if (navContract != null) {
|
||||||
enterHomeTransitionManager.resolve(navContract, window)
|
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() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
enterHomeTransitionManager.clear()
|
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
|
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.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
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.ColorScheme
|
||||||
import de.mm20.launcher2.preferences.GestureAction
|
import de.mm20.launcher2.preferences.GestureAction
|
||||||
import de.mm20.launcher2.preferences.ScreenOrientation
|
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.GestureSettings
|
||||||
import de.mm20.launcher2.preferences.ui.UiSettings
|
import de.mm20.launcher2.preferences.ui.UiSettings
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.ui.gestures.Gesture
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@ -29,7 +19,6 @@ import kotlinx.coroutines.flow.combine
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -37,8 +26,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
private val uiSettings: UiSettings by inject()
|
private val uiSettings: UiSettings by inject()
|
||||||
private val gestureSettings: GestureSettings 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 val searchableRepository: SavableSearchableRepository by inject()
|
||||||
|
|
||||||
private var isSystemInDarkMode = MutableStateFlow(false)
|
private var isSystemInDarkMode = MutableStateFlow(false)
|
||||||
@ -69,8 +56,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
isSystemInDarkMode.value = darkMode
|
isSystemInDarkMode.value = darkMode
|
||||||
}
|
}
|
||||||
|
|
||||||
val baseLayout = uiSettings.baseLayout
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
|
||||||
val bottomSearchBar = uiSettings.bottomSearchBar
|
val bottomSearchBar = uiSettings.bottomSearchBar
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||||
val reverseSearchResults = uiSettings.reverseSearchResults
|
val reverseSearchResults = uiSettings.reverseSearchResults
|
||||||
@ -81,49 +66,11 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
.map { it != ScreenOrientation.Auto }
|
.map { it != ScreenOrientation.Auto }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||||
|
|
||||||
val isSearchOpen = mutableStateOf(false)
|
val widgetsOnHomeScreen = uiSettings.homeScreenWidgets
|
||||||
val isWidgetEditMode = mutableStateOf(false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
val searchBarFocused = mutableStateOf(false)
|
|
||||||
|
|
||||||
val autoFocusSearch = uiSettings.openKeyboardOnSearch
|
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
|
val wallpaperBlur = uiSettings.blurWallpaper
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
|
||||||
val wallpaperBlurRadius = uiSettings.wallpaperBlurRadius
|
val wallpaperBlurRadius = uiSettings.wallpaperBlurRadius
|
||||||
@ -137,34 +84,27 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
val searchBarStyle = uiSettings.searchBarStyle
|
val searchBarStyle = uiSettings.searchBarStyle
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchBarStyle.Transparent)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchBarStyle.Transparent)
|
||||||
|
|
||||||
val gestureState: StateFlow<GestureState> = gestureSettings
|
val gestureState: StateFlow<GestureState?> = gestureSettings.map { settings ->
|
||||||
.combine(baseLayout) { settings, layout ->
|
val swipeLeftAction = settings.swipeLeft
|
||||||
val swipeLeftAction =
|
val swipeRightAction = settings.swipeRight
|
||||||
settings.swipeLeft.takeIf { layout != BaseLayout.Pager } ?: GestureAction.NoAction
|
val swipeDownAction = settings.swipeDown
|
||||||
val swipeRightAction = settings.swipeRight.takeIf { layout != BaseLayout.PagerReversed }
|
val swipeUpAction = settings.swipeUp
|
||||||
?: GestureAction.NoAction
|
|
||||||
val swipeDownAction =
|
|
||||||
settings.swipeDown.takeIf { layout != BaseLayout.PullDown } ?: GestureAction.NoAction
|
|
||||||
val longPressAction = settings.longPress
|
val longPressAction = settings.longPress
|
||||||
val doubleTapAction = settings.doubleTap
|
val doubleTapAction = settings.doubleTap
|
||||||
val homeButtonAction = settings.homeButton
|
val homeButtonAction = settings.homeButton
|
||||||
|
|
||||||
val swipeLeftAppKey =
|
val swipeLeftAppKey = (swipeLeftAction as? GestureAction.Launch)?.key
|
||||||
if (swipeLeftAction is GestureAction.Launch) swipeLeftAction.key else null
|
val swipeRightAppKey = (swipeRightAction as? GestureAction.Launch)?.key
|
||||||
val swipeRightAppKey =
|
val swipeDownAppKey = (swipeDownAction as? GestureAction.Launch)?.key
|
||||||
if (swipeRightAction is GestureAction.Launch) swipeRightAction.key else null
|
val swipeUpAppKey = (swipeUpAction as? GestureAction.Launch)?.key
|
||||||
val swipeDownAppKey =
|
val longPressAppKey = (longPressAction as? GestureAction.Launch)?.key
|
||||||
if (swipeDownAction is GestureAction.Launch) swipeDownAction.key else null
|
val doubleTapAppKey = (doubleTapAction as? GestureAction.Launch)?.key
|
||||||
val longPressAppKey =
|
val homeButtonAppKey = (homeButtonAction as? GestureAction.Launch)?.key
|
||||||
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 apps = listOfNotNull(
|
val apps = listOfNotNull(
|
||||||
swipeLeftAppKey,
|
swipeLeftAppKey,
|
||||||
swipeRightAppKey,
|
swipeRightAppKey,
|
||||||
swipeDownAppKey,
|
swipeDownAppKey,
|
||||||
|
swipeUpAppKey,
|
||||||
longPressAppKey,
|
longPressAppKey,
|
||||||
doubleTapAppKey,
|
doubleTapAppKey,
|
||||||
homeButtonAppKey,
|
homeButtonAppKey,
|
||||||
@ -174,117 +114,35 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
swipeLeftAction = swipeLeftAction,
|
swipeLeftAction = swipeLeftAction,
|
||||||
swipeRightAction = swipeRightAction,
|
swipeRightAction = swipeRightAction,
|
||||||
swipeDownAction = swipeDownAction,
|
swipeDownAction = swipeDownAction,
|
||||||
|
swipeUpAction = swipeUpAction,
|
||||||
longPressAction = longPressAction,
|
longPressAction = longPressAction,
|
||||||
doubleTapAction = doubleTapAction,
|
doubleTapAction = doubleTapAction,
|
||||||
homeButtonAction = homeButtonAction,
|
homeButtonAction = homeButtonAction,
|
||||||
swipeLeftApp = apps.firstOrNull { it.key == swipeLeftAppKey },
|
swipeLeftApp = apps.find { it.key == swipeLeftAppKey },
|
||||||
swipeRightApp = apps.firstOrNull { it.key == swipeRightAppKey },
|
swipeRightApp = apps.find { it.key == swipeRightAppKey },
|
||||||
swipeDownApp = apps.firstOrNull { it.key == swipeDownAppKey },
|
swipeDownApp = apps.find { it.key == swipeDownAppKey },
|
||||||
longPressApp = apps.firstOrNull { it.key == longPressAppKey },
|
swipeUpApp = apps.find { it.key == swipeUpAppKey },
|
||||||
doubleTapApp = apps.firstOrNull { it.key == doubleTapAppKey },
|
longPressApp = apps.find { it.key == longPressAppKey },
|
||||||
homeButtonApp = apps.firstOrNull { it.key == homeButtonAppKey },
|
doubleTapApp = apps.find { it.key == doubleTapAppKey },
|
||||||
|
homeButtonApp = apps.find { it.key == homeButtonAppKey },
|
||||||
)
|
)
|
||||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, GestureState())
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GestureState(
|
data class GestureState(
|
||||||
val swipeLeftAction: GestureAction = GestureAction.NoAction,
|
val swipeLeftAction: GestureAction = GestureAction.NoAction,
|
||||||
val swipeRightAction: GestureAction = GestureAction.NoAction,
|
val swipeRightAction: GestureAction = GestureAction.NoAction,
|
||||||
val swipeDownAction: GestureAction = GestureAction.NoAction,
|
val swipeDownAction: GestureAction = GestureAction.NoAction,
|
||||||
|
val swipeUpAction: GestureAction = GestureAction.NoAction,
|
||||||
val longPressAction: GestureAction = GestureAction.NoAction,
|
val longPressAction: GestureAction = GestureAction.NoAction,
|
||||||
val doubleTapAction: GestureAction = GestureAction.NoAction,
|
val doubleTapAction: GestureAction = GestureAction.NoAction,
|
||||||
val homeButtonAction: GestureAction = GestureAction.NoAction,
|
val homeButtonAction: GestureAction = GestureAction.NoAction,
|
||||||
val swipeLeftApp: SavableSearchable? = null,
|
val swipeLeftApp: SavableSearchable? = null,
|
||||||
val swipeRightApp: SavableSearchable? = null,
|
val swipeRightApp: SavableSearchable? = null,
|
||||||
val swipeDownApp: SavableSearchable? = null,
|
val swipeDownApp: SavableSearchable? = null,
|
||||||
|
val swipeUpApp: SavableSearchable? = null,
|
||||||
val longPressApp: SavableSearchable? = null,
|
val longPressApp: SavableSearchable? = null,
|
||||||
val doubleTapApp: SavableSearchable? = null,
|
val doubleTapApp: SavableSearchable? = null,
|
||||||
val homeButtonApp: 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.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
@ -34,23 +33,41 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.preferences.BaseLayout
|
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.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.BaseActivity
|
||||||
import de.mm20.launcher2.ui.base.ProvideCompositionLocals
|
import de.mm20.launcher2.ui.base.ProvideCompositionLocals
|
||||||
import de.mm20.launcher2.ui.component.NavBarEffects
|
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.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.LauncherBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets
|
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransition
|
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransition
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionManager
|
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionManager
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.LocalEnterHomeTransitionManager
|
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.LocalPreferDarkContentOverWallpaper
|
||||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||||
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
||||||
@ -66,13 +83,15 @@ abstract class SharedLauncherActivity(
|
|||||||
) : BaseActivity() {
|
) : BaseActivity() {
|
||||||
|
|
||||||
private val viewModel: LauncherScaffoldVM by viewModels()
|
private val viewModel: LauncherScaffoldVM by viewModels()
|
||||||
private val searchVM: SearchVM by viewModels()
|
|
||||||
|
|
||||||
internal val enterHomeTransitionManager = EnterHomeTransitionManager()
|
internal val enterHomeTransitionManager = EnterHomeTransitionManager()
|
||||||
|
|
||||||
internal val gestureDetector = GestureDetector()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
if (isAtLeastApiLevel(29)) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
window.isStatusBarContrastEnforced = false
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val wallpaperManager = WallpaperManager.getInstance(this)
|
val wallpaperManager = WallpaperManager.getInstance(this)
|
||||||
@ -98,7 +117,6 @@ abstract class SharedLauncherActivity(
|
|||||||
LocalWallpaperColors provides wallpaperColors,
|
LocalWallpaperColors provides wallpaperColors,
|
||||||
LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText),
|
LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText),
|
||||||
LocalBottomSheetManager provides bottomSheetManager,
|
LocalBottomSheetManager provides bottomSheetManager,
|
||||||
LocalGestureDetector provides gestureDetector,
|
|
||||||
) {
|
) {
|
||||||
LauncherTheme {
|
LauncherTheme {
|
||||||
ProvideCompositionLocals {
|
ProvideCompositionLocals {
|
||||||
@ -114,13 +132,20 @@ abstract class SharedLauncherActivity(
|
|||||||
|
|
||||||
val hideStatus by viewModel.hideStatusBar.collectAsState()
|
val hideStatus by viewModel.hideStatusBar.collectAsState()
|
||||||
val hideNav by viewModel.hideNavBar.collectAsState()
|
val hideNav by viewModel.hideNavBar.collectAsState()
|
||||||
val layout by viewModel.baseLayout.collectAsState(null)
|
|
||||||
val bottomSearchBar by viewModel.bottomSearchBar.collectAsState()
|
val bottomSearchBar by viewModel.bottomSearchBar.collectAsState()
|
||||||
val reverseSearchResults by viewModel.reverseSearchResults.collectAsState()
|
val reverseSearchResults by viewModel.reverseSearchResults.collectAsState()
|
||||||
val fixedSearchBar by viewModel.fixedSearchBar.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 fixedRotation by viewModel.fixedRotation.collectAsState()
|
||||||
|
|
||||||
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
|
||||||
|
if (gestures == null || widgetsOnHomeScreen == null) return@ProvideCompositionLocals
|
||||||
|
|
||||||
LaunchedEffect(fixedRotation) {
|
LaunchedEffect(fixedRotation) {
|
||||||
requestedOrientation = if (fixedRotation) {
|
requestedOrientation = if (fixedRotation) {
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
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) }
|
val enterTransitionProgress = remember { mutableStateOf(1f) }
|
||||||
var enterTransition by remember {
|
var enterTransition by remember {
|
||||||
@ -153,83 +195,191 @@ abstract class SharedLauncherActivity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(hideStatus) {
|
|
||||||
systemUiController.isStatusBarVisible = !hideStatus
|
|
||||||
}
|
|
||||||
LaunchedEffect(hideNav) {
|
|
||||||
systemUiController.isNavigationBarVisible = !hideNav
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayHost(
|
OverlayHost(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
|
|
||||||
contentAlignment = Alignment.BottomCenter
|
contentAlignment = Alignment.BottomCenter
|
||||||
) {
|
) {
|
||||||
if (chargingAnimation == true) {
|
if (chargingAnimation == true) {
|
||||||
NavBarEffects(modifier = Modifier.fillMaxSize())
|
NavBarEffects(modifier = Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
if (mode == LauncherActivityMode.Assistant) {
|
|
||||||
key(bottomSearchBar, reverseSearchResults) {
|
val config = remember(
|
||||||
AssistantScaffold(
|
mode,
|
||||||
modifier = Modifier
|
reverseSearchResults,
|
||||||
.fillMaxSize(),
|
bottomSearchBar,
|
||||||
|
fixedSearchBar,
|
||||||
|
gestures,
|
||||||
|
searchBarStyle,
|
||||||
|
darkSearchBar,
|
||||||
|
backgroundColor,
|
||||||
|
lightStatus,
|
||||||
|
lightNav,
|
||||||
|
hideStatus,
|
||||||
|
hideNav,
|
||||||
|
widgetsOnHomeScreen,
|
||||||
|
) {
|
||||||
|
if (mode == LauncherActivityMode.Assistant) {
|
||||||
|
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 {
|
||||||
|
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,
|
darkStatusBarIcons = lightStatus,
|
||||||
darkNavBarIcons = lightNav,
|
darkNavBarIcons = lightNav,
|
||||||
bottomSearchBar = bottomSearchBar,
|
backgroundColor = backgroundColor,
|
||||||
reverseSearchResults = reverseSearchResults,
|
showStatusBar = !hideStatus,
|
||||||
fixedSearchBar = fixedSearchBar,
|
showNavBar = !hideNav,
|
||||||
|
darkSearchBar = darkSearchBar,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (layout) {
|
|
||||||
BaseLayout.PullDown -> {
|
|
||||||
key(bottomSearchBar, reverseSearchResults) {
|
|
||||||
PullDownScaffold(
|
|
||||||
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,
|
|
||||||
bottomSearchBar = bottomSearchBar,
|
|
||||||
reverseSearchResults = reverseSearchResults,
|
|
||||||
fixedSearchBar = fixedSearchBar,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseLayout.Pager,
|
if (config.isUseless()) config.copy(
|
||||||
BaseLayout.PagerReversed -> {
|
homeComponent = SecretComponent,
|
||||||
key(bottomSearchBar, reverseSearchResults) {
|
) else config
|
||||||
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 -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LauncherScaffold(
|
||||||
|
config = config,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX =
|
||||||
|
0.5f + enterTransitionProgress.value * 0.5f
|
||||||
|
scaleY =
|
||||||
|
0.5f + enterTransitionProgress.value * 0.5f
|
||||||
|
alpha = enterTransitionProgress.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SnackbarHost(
|
SnackbarHost(
|
||||||
snackbarHostState,
|
snackbarHostState,
|
||||||
modifier = Modifier
|
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() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
val windowController = WindowCompat.getInsetsController(window, window.decorView.rootView)
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
|
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchColumn(
|
fun SearchColumn(
|
||||||
@ -369,7 +370,7 @@ fun LazyListScope.SingleResult(
|
|||||||
vertical = 4.dp,
|
vertical = 4.dp,
|
||||||
),
|
),
|
||||||
color = if (highlight) MaterialTheme.colorScheme.secondaryContainer
|
color = if (highlight) MaterialTheme.colorScheme.secondaryContainer
|
||||||
else MaterialTheme.colorScheme.surface.copy(LocalCardStyle.current.opacity)
|
else MaterialTheme.colorScheme.surface.copy(LocalTransparencyScheme.current.surface)
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.ui.ktx.withCorners
|
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
|
import kotlin.math.ceil
|
||||||
|
|
||||||
fun <T : SavableSearchable> LazyListScope.GridResults(
|
fun <T : SavableSearchable> LazyListScope.GridResults(
|
||||||
@ -39,7 +39,7 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
|
|||||||
bottom = if (!reverse && isBottom) 8.dp else 0.dp,
|
bottom = if (!reverse && isBottom) 8.dp else 0.dp,
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
|
MaterialTheme.colorScheme.surface.copy(alpha = LocalTransparencyScheme.current.surface),
|
||||||
MaterialTheme.shapes.medium.withCorners(
|
MaterialTheme.shapes.medium.withCorners(
|
||||||
topStart = isTop,
|
topStart = isTop,
|
||||||
topEnd = isTop,
|
topEnd = isTop,
|
||||||
@ -72,7 +72,7 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
|
|||||||
bottom = if (!reverse && isLast) 8.dp else 0.dp,
|
bottom = if (!reverse && isLast) 8.dp else 0.dp,
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
|
MaterialTheme.colorScheme.surface.copy(alpha = LocalTransparencyScheme.current.surface),
|
||||||
MaterialTheme.shapes.medium.withCorners(
|
MaterialTheme.shapes.medium.withCorners(
|
||||||
topStart = isFirst && !reverse || isLast && reverse,
|
topStart = isFirst && !reverse || isLast && reverse,
|
||||||
topEnd = 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,
|
bottom = if (!reverse) 8.dp else 0.dp,
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
|
MaterialTheme.colorScheme.surface.copy(alpha = LocalTransparencyScheme.current.surface),
|
||||||
MaterialTheme.shapes.medium.withCorners(
|
MaterialTheme.shapes.medium.withCorners(
|
||||||
topStart = isTop,
|
topStart = isTop,
|
||||||
topEnd = isTop,
|
topEnd = isTop,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.ui.ktx.animateCorners
|
import de.mm20.launcher2.ui.ktx.animateCorners
|
||||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
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(
|
fun <T : SavableSearchable> LazyListScope.ListResults(
|
||||||
key: String,
|
key: String,
|
||||||
@ -104,7 +104,7 @@ fun LazyItemScope.ListItemSurface(
|
|||||||
if (it) 2.dp else 0.dp
|
if (it) 2.dp else 0.dp
|
||||||
}
|
}
|
||||||
val backgroundAlpha by transition.animateFloat {
|
val backgroundAlpha by transition.animateFloat {
|
||||||
if (it) 1f else LocalCardStyle.current.opacity
|
if (it) 1f else LocalTransparencyScheme.current.surface
|
||||||
}
|
}
|
||||||
|
|
||||||
val padding by transition.animateDp {
|
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.component.Banner
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
||||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
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(
|
fun LazyListScope.SearchFavorites(
|
||||||
favorites: List<SavableSearchable>,
|
favorites: List<SavableSearchable>,
|
||||||
@ -47,7 +47,7 @@ fun LazyListScope.SearchFavorites(
|
|||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surface.copy(
|
MaterialTheme.colorScheme.surface.copy(
|
||||||
LocalCardStyle.current.opacity
|
LocalTransparencyScheme.current.surface
|
||||||
),
|
),
|
||||||
MaterialTheme.shapes.medium
|
MaterialTheme.shapes.medium
|
||||||
)
|
)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
|
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
|
||||||
import de.mm20.launcher2.search.SearchFilters
|
import de.mm20.launcher2.search.SearchFilters
|
||||||
|
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KeyboardFilterBar(
|
fun KeyboardFilterBar(
|
||||||
@ -43,6 +44,7 @@ fun KeyboardFilterBar(
|
|||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.consumeAllScrolling()
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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.FilterAlt
|
||||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.FilledIconButton
|
|
||||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.IconToggleButton
|
import androidx.compose.material3.IconToggleButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -52,11 +49,10 @@ fun LauncherSearchBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
style: SearchBarStyle,
|
style: SearchBarStyle,
|
||||||
level: () -> SearchBarLevel,
|
level: () -> SearchBarLevel,
|
||||||
value: () -> String,
|
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
onFocusChange: (Boolean) -> Unit,
|
onFocusChange: (Boolean) -> Unit,
|
||||||
actions: List<SearchAction>,
|
actions: List<SearchAction>,
|
||||||
highlightedAction: SearchAction?,
|
highlightedAction: SearchAction? = null,
|
||||||
isSearchOpen: Boolean = false,
|
isSearchOpen: Boolean = false,
|
||||||
darkColors: Boolean = false,
|
darkColors: Boolean = false,
|
||||||
bottomSearchBar: Boolean = false,
|
bottomSearchBar: Boolean = false,
|
||||||
@ -77,18 +73,15 @@ fun LauncherSearchBar(
|
|||||||
else focusManager.clearFocus()
|
else focusManager.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
val filterBar by searchVM.filterBar.collectAsState(false)
|
val value by searchVM.searchQuery
|
||||||
|
|
||||||
val _value = value()
|
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
SearchBar(
|
SearchBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
|
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.offset { IntOffset(0, searchBarOffset()) },
|
.offset { IntOffset(0, searchBarOffset()) },
|
||||||
style = style, level = level(), value = _value, onValueChange = {
|
style = style, level = level(), value = value, onValueChange = {
|
||||||
searchVM.search(it)
|
searchVM.search(it)
|
||||||
},
|
},
|
||||||
reverse = bottomSearchBar,
|
reverse = bottomSearchBar,
|
||||||
@ -136,7 +129,7 @@ fun LauncherSearchBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SearchBarMenu(searchBarValue = _value, onInputClear = {
|
SearchBarMenu(searchBarValue = value, onInputClear = {
|
||||||
searchVM.reset()
|
searchVM.reset()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -152,22 +145,5 @@ fun LauncherSearchBar(
|
|||||||
onUnfocus = { onFocusChange(false) },
|
onUnfocus = { onFocusChange(false) },
|
||||||
onKeyboardActionGo = onKeyboardActionGo
|
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 androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.ui.component.SearchActionIcon
|
import de.mm20.launcher2.ui.component.SearchActionIcon
|
||||||
|
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
|
||||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -37,6 +38,7 @@ fun ColumnScope.SearchBarActions(
|
|||||||
AnimatedVisibility(actions.isNotEmpty()) {
|
AnimatedVisibility(actions.isNotEmpty()) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.consumeAllScrolling()
|
||||||
.height(48.dp)
|
.height(48.dp)
|
||||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp),
|
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@ -77,22 +77,6 @@ fun RowScope.SearchBarMenu(
|
|||||||
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
|
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(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
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.R
|
||||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||||
import de.mm20.launcher2.ui.gestures.Gesture
|
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
|
||||||
import de.mm20.launcher2.ui.launcher.FailedGesture
|
|
||||||
|
data class FailedGesture(val gesture: Gesture, val action: GestureAction)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FailedGestureSheet(
|
fun FailedGestureSheet(
|
||||||
@ -43,7 +44,9 @@ fun FailedGestureSheet(
|
|||||||
Gesture.SwipeDown -> R.string.preference_gesture_swipe_down
|
Gesture.SwipeDown -> R.string.preference_gesture_swipe_down
|
||||||
Gesture.SwipeLeft -> R.string.preference_gesture_swipe_left
|
Gesture.SwipeLeft -> R.string.preference_gesture_swipe_left
|
||||||
Gesture.SwipeRight -> R.string.preference_gesture_swipe_right
|
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(
|
BottomSheetDialog(
|
||||||
|
|||||||
@ -2,13 +2,11 @@ package de.mm20.launcher2.ui.launcher.sheets
|
|||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.GestureAction
|
import de.mm20.launcher2.preferences.GestureAction
|
||||||
import de.mm20.launcher2.preferences.ui.GestureSettings
|
import de.mm20.launcher2.preferences.ui.GestureSettings
|
||||||
import de.mm20.launcher2.ui.gestures.Gesture
|
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -27,7 +25,8 @@ class FailedGestureSheetVM : ViewModel(), KoinComponent {
|
|||||||
Gesture.SwipeDown -> gestureSettings.setSwipeDown(GestureAction.NoAction)
|
Gesture.SwipeDown -> gestureSettings.setSwipeDown(GestureAction.NoAction)
|
||||||
Gesture.SwipeLeft -> gestureSettings.setSwipeLeft(GestureAction.NoAction)
|
Gesture.SwipeLeft -> gestureSettings.setSwipeLeft(GestureAction.NoAction)
|
||||||
Gesture.SwipeRight -> gestureSettings.setSwipeRight(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.lifecycle.LifecycleEventObserver
|
||||||
import androidx.savedstate.SavedStateRegistry
|
import androidx.savedstate.SavedStateRegistry
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import de.mm20.launcher2.preferences.GestureAction
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
|
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
|
||||||
|
|
||||||
class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
|
class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
|
||||||
SavedStateRegistry.SavedStateProvider {
|
SavedStateRegistry.SavedStateProvider {
|
||||||
@ -16,6 +18,7 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
|
|||||||
val editFavoritesSheetShown = mutableStateOf(false)
|
val editFavoritesSheetShown = mutableStateOf(false)
|
||||||
val hiddenItemsSheetShown = mutableStateOf(false)
|
val hiddenItemsSheetShown = mutableStateOf(false)
|
||||||
val editTagSheetShown = mutableStateOf<String?>(null)
|
val editTagSheetShown = mutableStateOf<String?>(null)
|
||||||
|
val failedGestureSheetShown = mutableStateOf<FailedGesture?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||||
@ -73,6 +76,13 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
|
|||||||
editTagSheetShown.value = null
|
editTagSheetShown.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showFailedGestureSheet(gesture: Gesture, action: GestureAction) {
|
||||||
|
failedGestureSheetShown.value = FailedGesture(gesture, action)
|
||||||
|
}
|
||||||
|
fun dismissFailedGestureSheet() {
|
||||||
|
failedGestureSheetShown.value = null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PROVIDER = "bottom_sheet_manager"
|
private const val PROVIDER = "bottom_sheet_manager"
|
||||||
private const val FAVORITES = "favorites"
|
private const val FAVORITES = "favorites"
|
||||||
|
|||||||
@ -16,4 +16,7 @@ fun LauncherBottomSheets() {
|
|||||||
bottomSheetManager.editTagSheetShown.value?.let {
|
bottomSheetManager.editTagSheetShown.value?.let {
|
||||||
EditTagSheet(tag = it, onDismiss = { bottomSheetManager.dismissEditTagSheet() })
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
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.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
@ -114,7 +118,7 @@ fun WidgetColumn(
|
|||||||
swapThresholds[i][0] = it.positionInParent().y
|
swapThresholds[i][0] = it.positionInParent().y
|
||||||
swapThresholds[i][1] = it.positionInParent().y + it.size.height
|
swapThresholds[i][1] = it.positionInParent().y + it.size.height
|
||||||
}
|
}
|
||||||
.padding(top = 8.dp)
|
.padding(top = if (i > 0) 8.dp else 0.dp)
|
||||||
.offset {
|
.offset {
|
||||||
IntOffset(0, offsetY.value.toInt())
|
IntOffset(0, offsetY.value.toInt())
|
||||||
},
|
},
|
||||||
@ -156,31 +160,28 @@ fun WidgetColumn(
|
|||||||
)
|
)
|
||||||
val icon =
|
val icon =
|
||||||
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
|
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
modifier = Modifier
|
Button(
|
||||||
.semantics {
|
modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 8.dp),
|
||||||
role = Role.Button
|
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||||
contentDescription = title
|
onClick = {
|
||||||
}
|
|
||||||
.padding(16.dp)
|
|
||||||
.align(Alignment.CenterHorizontally),
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painter = rememberAnimatedVectorPainter(
|
|
||||||
animatedImageVector = icon,
|
|
||||||
atEnd = !editMode
|
|
||||||
), contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(title)
|
|
||||||
}, onClick = {
|
|
||||||
if (!editMode) {
|
if (!editMode) {
|
||||||
onEditModeChange(true)
|
onEditModeChange(true)
|
||||||
} else {
|
} else {
|
||||||
addNewWidget = true
|
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.gestures.rememberDraggableState
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.DragIndicator
|
import androidx.compose.material.icons.rounded.DragIndicator
|
||||||
import androidx.compose.material.icons.rounded.Tune
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
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.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import de.mm20.launcher2.ui.R
|
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.component.LauncherCard
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.ConfigureWidgetSheet
|
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.calendar.CalendarWidget
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.external.AppWidget
|
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.favorites.FavoritesWidget
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
|
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.notes.NotesWidget
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
|
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.AppWidget
|
||||||
import de.mm20.launcher2.widgets.CalendarWidget
|
import de.mm20.launcher2.widgets.CalendarWidget
|
||||||
import de.mm20.launcher2.widgets.FavoritesWidget
|
import de.mm20.launcher2.widgets.FavoritesWidget
|
||||||
@ -72,14 +66,14 @@ fun WidgetItem(
|
|||||||
var configure by rememberSaveable { mutableStateOf(false) }
|
var configure by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
var isDragged by remember { 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) {
|
val appWidget = if (widget is AppWidget) remember(widget.config.widgetId) {
|
||||||
AppWidgetManager.getInstance(context).getAppWidgetInfo(widget.config.widgetId)
|
AppWidgetManager.getInstance(context).getAppWidgetInfo(widget.config.widgetId)
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
val backgroundOpacity by animateFloatAsState(
|
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",
|
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.BatteryFull
|
||||||
import androidx.compose.material.icons.rounded.ColorLens
|
import androidx.compose.material.icons.rounded.ColorLens
|
||||||
import androidx.compose.material.icons.rounded.DarkMode
|
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.HorizontalSplit
|
||||||
import androidx.compose.material.icons.rounded.LightMode
|
import androidx.compose.material.icons.rounded.LightMode
|
||||||
import androidx.compose.material.icons.rounded.MusicNote
|
import androidx.compose.material.icons.rounded.MusicNote
|
||||||
@ -563,21 +562,13 @@ fun ConfigureClockWidgetSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedCard(
|
if (fillHeight == true) {
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
OutlinedCard(
|
||||||
) {
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
) {
|
||||||
SwitchPreference(
|
Column(
|
||||||
title = stringResource(R.string.preference_clock_widget_fill_height),
|
modifier = Modifier.fillMaxWidth()
|
||||||
icon = Icons.Rounded.Height,
|
) {
|
||||||
value = fillHeight == true,
|
|
||||||
onValueChanged = {
|
|
||||||
viewModel.setFillHeight(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
AnimatedVisibility(fillHeight == true) {
|
|
||||||
var showDropdown by remember { mutableStateOf(false) }
|
var showDropdown by remember { mutableStateOf(false) }
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(R.string.preference_clock_widget_alignment),
|
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.ktx.conditional
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionParams
|
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionParams
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.HandleEnterHomeTransition
|
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.locals.LocalWindowSize
|
||||||
|
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
|
||||||
import de.mm20.launcher2.widgets.MusicWidget
|
import de.mm20.launcher2.widgets.MusicWidget
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ -352,7 +352,7 @@ fun MusicWidget(widget: MusicWidget) {
|
|||||||
) {
|
) {
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalCardStyle.current.opacity),
|
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalTransparencyScheme.current.surface),
|
||||||
),
|
),
|
||||||
onClick = { viewModel.togglePause() },
|
onClick = { viewModel.togglePause() },
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -5,13 +5,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.AnimatedWeatherIcon
|
||||||
import de.mm20.launcher2.ui.component.weather.WeatherIcon
|
import de.mm20.launcher2.ui.component.weather.WeatherIcon
|
||||||
import de.mm20.launcher2.ui.ktx.blendIntoViewScale
|
import de.mm20.launcher2.ui.ktx.blendIntoViewScale
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
|
||||||
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
|
|
||||||
import de.mm20.launcher2.weather.DailyForecast
|
import de.mm20.launcher2.weather.DailyForecast
|
||||||
import de.mm20.launcher2.weather.Forecast
|
import de.mm20.launcher2.weather.Forecast
|
||||||
import de.mm20.launcher2.widgets.WeatherWidget
|
import de.mm20.launcher2.widgets.WeatherWidget
|
||||||
@ -121,7 +118,9 @@ fun WeatherWidget(widget: WeatherWidget) {
|
|||||||
Column {
|
Column {
|
||||||
if (!isProviderAvailable) {
|
if (!isProviderAvailable) {
|
||||||
Banner(
|
Banner(
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
text = stringResource(R.string.weather_widget_no_provider),
|
text = stringResource(R.string.weather_widget_no_provider),
|
||||||
icon = Icons.Rounded.ErrorOutline,
|
icon = Icons.Rounded.ErrorOutline,
|
||||||
primaryAction = {
|
primaryAction = {
|
||||||
@ -134,7 +133,9 @@ fun WeatherWidget(widget: WeatherWidget) {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Rounded.OpenInNew,
|
Icons.AutoMirrored.Rounded.OpenInNew,
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.padding(end = ButtonDefaults.IconSpacing).size(ButtonDefaults.IconSize)
|
modifier = Modifier
|
||||||
|
.padding(end = ButtonDefaults.IconSpacing)
|
||||||
|
.size(ButtonDefaults.IconSize)
|
||||||
)
|
)
|
||||||
Text(stringResource(R.string.settings))
|
Text(stringResource(R.string.settings))
|
||||||
}
|
}
|
||||||
@ -179,7 +180,7 @@ fun WeatherWidget(widget: WeatherWidget) {
|
|||||||
val currentDayForecasts by viewModel.currentDayForecasts
|
val currentDayForecasts by viewModel.currentDayForecasts
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = LocalCardStyle.current.opacity),
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = LocalTransparencyScheme.current.surface),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -240,26 +241,26 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = weatherApp != null,
|
enabled = weatherApp != null,
|
||||||
onClick = {
|
onClick = {
|
||||||
context.tryStartActivity(
|
context.tryStartActivity(
|
||||||
Intent().also {
|
Intent().also {
|
||||||
it.component = weatherApp?.activityInfo?.let {
|
it.component = weatherApp?.activityInfo?.let {
|
||||||
ComponentName(it.packageName, it.name)
|
ComponentName(it.packageName, it.name)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ActivityOptionsCompat.makeClipRevealAnimation(
|
ActivityOptionsCompat.makeClipRevealAnimation(
|
||||||
view,
|
view,
|
||||||
bounds.left.toInt(),
|
bounds.left.toInt(),
|
||||||
bounds.top.toInt(),
|
bounds.top.toInt(),
|
||||||
bounds.width.toInt(),
|
bounds.width.toInt(),
|
||||||
bounds.height.toInt()
|
bounds.height.toInt()
|
||||||
).toBundle()
|
).toBundle()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = LocalIndication.current,
|
indication = LocalIndication.current,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@ -296,7 +297,7 @@ fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
|
|||||||
topEnd = CornerSize(0),
|
topEnd = CornerSize(0),
|
||||||
bottomEnd = 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(
|
||||||
text = "${forecast.provider} (${
|
text = "${forecast.provider} (${
|
||||||
@ -451,8 +452,7 @@ fun WeatherTimeSelector(
|
|||||||
LazyRow(
|
LazyRow(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.consumeAllScrolling(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp),
|
contentPadding = PaddingValues(start = 16.dp, end = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@ -476,7 +476,8 @@ fun WeatherTimeSelector(
|
|||||||
verticalArrangement = Arrangement.SpaceEvenly
|
verticalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
WeatherIcon(
|
WeatherIcon(
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = fc.condition
|
contentDescription = fc.condition
|
||||||
},
|
},
|
||||||
@ -515,8 +516,7 @@ fun WeatherDaySelector(
|
|||||||
LazyRow(
|
LazyRow(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.consumeAllScrolling(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp),
|
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 de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@ -71,9 +70,6 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val fillHeight = settings.fillHeight
|
val fillHeight = settings.fillHeight
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
fun setFillHeight(fillHeight: Boolean) {
|
|
||||||
settings.setFillHeight(fillHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
val parts = settings.parts
|
val parts = settings.parts
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import android.widget.Toast
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -17,10 +18,12 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.ui.BuildConfig
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||||
|
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|||||||
@ -3,7 +3,10 @@ package de.mm20.launcher2.ui.settings.debug
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.icons.IconService
|
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 de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -12,6 +15,9 @@ class DebugSettingsScreenVM: ViewModel(), KoinComponent {
|
|||||||
private val searchableRepository: SavableSearchableRepository by inject()
|
private val searchableRepository: SavableSearchableRepository by inject()
|
||||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||||
private val iconService: IconService by inject()
|
private val iconService: IconService by inject()
|
||||||
|
|
||||||
|
private val uiSettings: UiSettings by inject()
|
||||||
|
|
||||||
suspend fun cleanUpDatabase(): Int {
|
suspend fun cleanUpDatabase(): Int {
|
||||||
var removed = searchableRepository.cleanupDatabase()
|
var removed = searchableRepository.cleanupDatabase()
|
||||||
removed += customAttributesRepository.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.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
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.LocalContentColor
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -20,6 +27,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -27,7 +35,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.preferences.BaseLayout
|
|
||||||
import de.mm20.launcher2.preferences.GestureAction
|
import de.mm20.launcher2.preferences.GestureAction
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
@ -43,7 +50,6 @@ import de.mm20.launcher2.ui.ktx.toPixels
|
|||||||
fun GestureSettingsScreen() {
|
fun GestureSettingsScreen() {
|
||||||
val viewModel: GestureSettingsScreenVM = viewModel()
|
val viewModel: GestureSettingsScreenVM = viewModel()
|
||||||
|
|
||||||
val layout by viewModel.baseLayout.collectAsStateWithLifecycle(null)
|
|
||||||
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
|
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
val options = buildList {
|
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_recents) to GestureAction.Recents)
|
||||||
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
|
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_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))
|
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
PreferenceScreen(title = stringResource(R.string.preference_screen_gestures)) {
|
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 {
|
item {
|
||||||
val appIconSize = 32.dp.toPixels()
|
val appIconSize = 32.dp.toPixels()
|
||||||
PreferenceCategory {
|
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)
|
val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null)
|
||||||
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(doubleTap)) {
|
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(doubleTap)) {
|
||||||
MissingPermissionBanner(
|
MissingPermissionBanner(
|
||||||
@ -92,9 +177,9 @@ fun GestureSettingsScreen() {
|
|||||||
}.collectAsState(null)
|
}.collectAsState(null)
|
||||||
GesturePreference(
|
GesturePreference(
|
||||||
title = stringResource(R.string.preference_gesture_double_tap),
|
title = stringResource(R.string.preference_gesture_double_tap),
|
||||||
|
icon = Icons.Rounded.Adjust,
|
||||||
value = doubleTap,
|
value = doubleTap,
|
||||||
onValueChanged = { viewModel.setDoubleTap(it) },
|
onValueChanged = { viewModel.setDoubleTap(it) },
|
||||||
isOpenSearch = false,
|
|
||||||
options = options,
|
options = options,
|
||||||
app = doubleTapApp,
|
app = doubleTapApp,
|
||||||
appIcon = doubleTapAppIcon,
|
appIcon = doubleTapAppIcon,
|
||||||
@ -115,87 +200,15 @@ fun GestureSettingsScreen() {
|
|||||||
}.collectAsState(null)
|
}.collectAsState(null)
|
||||||
GesturePreference(
|
GesturePreference(
|
||||||
title = stringResource(R.string.preference_gesture_long_press),
|
title = stringResource(R.string.preference_gesture_long_press),
|
||||||
|
icon = Icons.Rounded.Circle,
|
||||||
value = longPress,
|
value = longPress,
|
||||||
onValueChanged = { viewModel.setLongPress(it) },
|
onValueChanged = { viewModel.setLongPress(it) },
|
||||||
isOpenSearch = false,
|
|
||||||
options = options,
|
options = options,
|
||||||
app = longPressApp,
|
app = longPressApp,
|
||||||
appIcon = longPressAppIcon,
|
appIcon = longPressAppIcon,
|
||||||
onAppChanged = { viewModel.setLongPressApp(it) }
|
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)
|
val homeButton by viewModel.homeButton.collectAsStateWithLifecycle(null)
|
||||||
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(homeButton)) {
|
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(homeButton)) {
|
||||||
MissingPermissionBanner(
|
MissingPermissionBanner(
|
||||||
@ -210,9 +223,9 @@ fun GestureSettingsScreen() {
|
|||||||
}.collectAsState(null)
|
}.collectAsState(null)
|
||||||
GesturePreference(
|
GesturePreference(
|
||||||
title = stringResource(R.string.preference_gesture_home_button),
|
title = stringResource(R.string.preference_gesture_home_button),
|
||||||
|
icon = Icons.Rounded.Home,
|
||||||
value = homeButton,
|
value = homeButton,
|
||||||
onValueChanged = { viewModel.setHomeButton(it) },
|
onValueChanged = { viewModel.setHomeButton(it) },
|
||||||
isOpenSearch = false,
|
|
||||||
options = options,
|
options = options,
|
||||||
app = homeButtonApp,
|
app = homeButtonApp,
|
||||||
appIcon = homeButtonAppIcon,
|
appIcon = homeButtonAppIcon,
|
||||||
@ -230,6 +243,7 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
|
|||||||
is GestureAction.QuickSettings,
|
is GestureAction.QuickSettings,
|
||||||
is GestureAction.Recents,
|
is GestureAction.Recents,
|
||||||
is GestureAction.PowerMenu -> true
|
is GestureAction.PowerMenu -> true
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,9 +251,9 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
|
|||||||
@Composable
|
@Composable
|
||||||
fun GesturePreference(
|
fun GesturePreference(
|
||||||
title: String,
|
title: String,
|
||||||
|
icon: ImageVector,
|
||||||
value: GestureAction?,
|
value: GestureAction?,
|
||||||
onValueChanged: (GestureAction) -> Unit,
|
onValueChanged: (GestureAction) -> Unit,
|
||||||
isOpenSearch: Boolean,
|
|
||||||
options: List<Pair<String, GestureAction>>,
|
options: List<Pair<String, GestureAction>>,
|
||||||
app: SavableSearchable?,
|
app: SavableSearchable?,
|
||||||
appIcon: LauncherIcon?,
|
appIcon: LauncherIcon?,
|
||||||
@ -254,14 +268,15 @@ fun GesturePreference(
|
|||||||
) {
|
) {
|
||||||
ListPreference(
|
ListPreference(
|
||||||
title = title,
|
title = title,
|
||||||
enabled = !isOpenSearch,
|
icon = icon,
|
||||||
items = options,
|
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) }
|
onValueChanged = { if (it != null) onValueChanged(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value is GestureAction.Launch && !isOpenSearch) {
|
if (value is GestureAction.Launch) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(36.dp)
|
.height(36.dp)
|
||||||
@ -269,15 +284,16 @@ fun GesturePreference(
|
|||||||
.alpha(0.38f)
|
.alpha(0.38f)
|
||||||
.background(LocalContentColor.current)
|
.background(LocalContentColor.current)
|
||||||
)
|
)
|
||||||
Box(modifier = Modifier
|
Box(
|
||||||
.clickable { showAppPicker = true }
|
modifier = Modifier
|
||||||
.padding(12.dp)) {
|
.clickable { showAppPicker = true }
|
||||||
|
.padding(12.dp)) {
|
||||||
ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
|
ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpenSearch && value is GestureAction.Launch && (showAppPicker || app == null)) {
|
if (value is GestureAction.Launch && (showAppPicker || app == null)) {
|
||||||
SearchablePicker(
|
SearchablePicker(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showAppPicker = false
|
showAppPicker = false
|
||||||
|
|||||||
@ -34,20 +34,14 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.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
|
val swipeDown = gestureSettings.swipeDown
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
val swipeLeft = gestureSettings.swipeLeft
|
val swipeLeft = gestureSettings.swipeLeft
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
val swipeRight = gestureSettings.swipeRight
|
val swipeRight = gestureSettings.swipeRight
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
val swipeUp = gestureSettings.swipeUp
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
val doubleTap = gestureSettings.doubleTap
|
val doubleTap = gestureSettings.doubleTap
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
val longPress = gestureSettings.longPress
|
val longPress = gestureSettings.longPress
|
||||||
@ -67,6 +61,10 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
gestureSettings.setSwipeRight(action)
|
gestureSettings.setSwipeRight(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSwipeUp(action: GestureAction) {
|
||||||
|
gestureSettings.setSwipeUp(action)
|
||||||
|
}
|
||||||
|
|
||||||
fun setDoubleTap(action: GestureAction) {
|
fun setDoubleTap(action: GestureAction) {
|
||||||
gestureSettings.setDoubleTap(action)
|
gestureSettings.setDoubleTap(action)
|
||||||
}
|
}
|
||||||
@ -121,6 +119,20 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
setSwipeDown(GestureAction.Launch(searchable.key))
|
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
|
val longPressApp: Flow<SavableSearchable?> = longPress
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
if (it !is GestureAction.Launch || it.key == null) flowOf(null)
|
if (it !is GestureAction.Launch || it.key == null) flowOf(null)
|
||||||
|
|||||||
@ -54,6 +54,7 @@ fun HomescreenSettingsScreen() {
|
|||||||
|
|
||||||
val dock by viewModel.dock.collectAsStateWithLifecycle(null)
|
val dock by viewModel.dock.collectAsStateWithLifecycle(null)
|
||||||
val fixedRotation by viewModel.fixedRotation.collectAsStateWithLifecycle(null)
|
val fixedRotation by viewModel.fixedRotation.collectAsStateWithLifecycle(null)
|
||||||
|
val widgetsOnHomeScreen by viewModel.widgetsOnHomeScreen.collectAsStateWithLifecycle(null)
|
||||||
val editButton by viewModel.widgetEditButton.collectAsStateWithLifecycle(null)
|
val editButton by viewModel.widgetEditButton.collectAsStateWithLifecycle(null)
|
||||||
val searchBarStyle by viewModel.searchBarStyle.collectAsStateWithLifecycle(null)
|
val searchBarStyle by viewModel.searchBarStyle.collectAsStateWithLifecycle(null)
|
||||||
val searchBarColor by viewModel.searchBarColor.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 {
|
item {
|
||||||
PreferenceCategory(
|
PreferenceCategory(
|
||||||
title = stringResource(id = R.string.preference_category_widgets)
|
title = stringResource(id = R.string.preference_category_widgets)
|
||||||
@ -104,6 +93,21 @@ fun HomescreenSettingsScreen() {
|
|||||||
viewModel.showClockWidgetSheet = true
|
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(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.preference_edit_button),
|
title = stringResource(id = R.string.preference_edit_button),
|
||||||
summary = stringResource(id = R.string.preference_widgets_edit_button_summary),
|
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.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.preferences.GestureAction
|
||||||
import de.mm20.launcher2.preferences.ScreenOrientation
|
import de.mm20.launcher2.preferences.ScreenOrientation
|
||||||
import de.mm20.launcher2.preferences.SearchBarColors
|
import de.mm20.launcher2.preferences.SearchBarColors
|
||||||
import de.mm20.launcher2.preferences.SearchBarStyle
|
import de.mm20.launcher2.preferences.SearchBarStyle
|
||||||
import de.mm20.launcher2.preferences.SystemBarColors
|
import de.mm20.launcher2.preferences.SystemBarColors
|
||||||
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
||||||
|
import de.mm20.launcher2.preferences.ui.GestureSettings
|
||||||
import de.mm20.launcher2.preferences.ui.UiSettings
|
import de.mm20.launcher2.preferences.ui.UiSettings
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -29,6 +32,7 @@ import org.koin.core.component.get
|
|||||||
class HomescreenSettingsScreenVM(
|
class HomescreenSettingsScreenVM(
|
||||||
private val uiSettings: UiSettings,
|
private val uiSettings: UiSettings,
|
||||||
private val clockWidgetSettings: ClockWidgetSettings,
|
private val clockWidgetSettings: ClockWidgetSettings,
|
||||||
|
private val gestureSettings: GestureSettings,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var showClockWidgetSheet by mutableStateOf(false)
|
var showClockWidgetSheet by mutableStateOf(false)
|
||||||
@ -120,11 +124,11 @@ class HomescreenSettingsScreenVM(
|
|||||||
uiSettings.setBottomSearchBar(bottomSearchBar)
|
uiSettings.setBottomSearchBar(bottomSearchBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dock = clockWidgetSettings.dock
|
val dock = uiSettings.dock
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
fun setDock(dock: Boolean) {
|
fun setDock(dock: Boolean) {
|
||||||
clockWidgetSettings.setDock(dock)
|
uiSettings.setDock(dock)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fixedRotation = uiSettings.orientation.map { it != ScreenOrientation.Auto }
|
val fixedRotation = uiSettings.orientation.map { it != ScreenOrientation.Auto }
|
||||||
@ -148,12 +152,52 @@ class HomescreenSettingsScreenVM(
|
|||||||
uiSettings.setChargingAnimation(chargingAnimation)
|
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 {
|
companion object : KoinComponent {
|
||||||
val Factory = viewModelFactory {
|
val Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
HomescreenSettingsScreenVM(
|
HomescreenSettingsScreenVM(
|
||||||
uiSettings = get(),
|
uiSettings = get(),
|
||||||
clockWidgetSettings = 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.Info
|
||||||
import androidx.compose.material.icons.rounded.Palette
|
import androidx.compose.material.icons.rounded.Palette
|
||||||
import androidx.compose.material.icons.rounded.Power
|
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.Search
|
||||||
import androidx.compose.material.icons.rounded.SettingsBackupRestore
|
import androidx.compose.material.icons.rounded.SettingsBackupRestore
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|||||||
@ -48,8 +48,8 @@ fun wallpaperColorsAsState(): State<WallpaperColors> {
|
|||||||
if (isAtLeastApiLevel(27)) {
|
if (isAtLeastApiLevel(27)) {
|
||||||
DisposableEffect(null) {
|
DisposableEffect(null) {
|
||||||
val wallpaperManager = WallpaperManager.getInstance(context)
|
val wallpaperManager = WallpaperManager.getInstance(context)
|
||||||
val callback = callback@{ colors: android.app.WallpaperColors?, which: Int ->
|
val callback = WallpaperManager.OnColorsChangedListener { colors, which ->
|
||||||
if (which and WallpaperManager.FLAG_SYSTEM == 0) return@callback
|
if (which and WallpaperManager.FLAG_SYSTEM == 0) return@OnColorsChangedListener
|
||||||
if (colors != null) {
|
if (colors != null) {
|
||||||
state.value = WallpaperColors.fromPlatformType(colors)
|
state.value = WallpaperColors.fromPlatformType(colors)
|
||||||
} else {
|
} 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>
|
<item name="android:windowIsTranslucent">true</item>
|
||||||
</style>
|
</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">
|
<style name="DialogTheme" parent="BaseTheme">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
|
|||||||
@ -668,6 +668,8 @@
|
|||||||
<string name="preference_compact_tags">Compact tags</string>
|
<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_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_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">Home screen</string>
|
||||||
<string name="preference_screen_homescreen_summary">Clock, search bar, wallpaper, system bars</string>
|
<string name="preference_screen_homescreen_summary">Clock, search bar, wallpaper, system bars</string>
|
||||||
<string name="preference_screen_icons">Grid & icons</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_down">Swipe down</string>
|
||||||
<string name="preference_gesture_swipe_left">Swipe left</string>
|
<string name="preference_gesture_swipe_left">Swipe left</string>
|
||||||
<string name="preference_gesture_swipe_right">Swipe right</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_double_tap">Double tap</string>
|
||||||
<string name="preference_gesture_long_press">Long press</string>
|
<string name="preference_gesture_long_press">Long press</string>
|
||||||
<string name="preference_gesture_home_button">Home button/gesture</string>
|
<string name="preference_gesture_home_button">Home button/gesture</string>
|
||||||
<string name="gesture_action_none">Do nothing</string>
|
<string name="gesture_action_none">Do nothing</string>
|
||||||
<string name="gesture_action_open_search">Open search</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_launch_app">Launch app</string>
|
||||||
<string name="gesture_action_notifications">Open notification drawer</string>
|
<string name="gesture_action_notifications">Open notification drawer</string>
|
||||||
<string name="gesture_action_lock_screen">Turn off screen</string>
|
<string name="gesture_action_lock_screen">Turn off screen</string>
|
||||||
@ -1023,4 +1027,6 @@
|
|||||||
<item quantity="other">in %1$d minutes</item>
|
<item quantity="other">in %1$d minutes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="departure_time_departed">departed</string>
|
<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>
|
</resources>
|
||||||
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import de.mm20.launcher2.preferences.migrations.Migration2
|
import de.mm20.launcher2.preferences.migrations.Migration2
|
||||||
import de.mm20.launcher2.preferences.migrations.Migration3
|
import de.mm20.launcher2.preferences.migrations.Migration3
|
||||||
import de.mm20.launcher2.preferences.migrations.Migration4
|
import de.mm20.launcher2.preferences.migrations.Migration4
|
||||||
|
import de.mm20.launcher2.preferences.migrations.Migration5
|
||||||
import de.mm20.launcher2.settings.BaseSettings
|
import de.mm20.launcher2.settings.BaseSettings
|
||||||
|
|
||||||
internal class LauncherDataStore(
|
internal class LauncherDataStore(
|
||||||
@ -16,6 +17,7 @@ internal class LauncherDataStore(
|
|||||||
Migration2(),
|
Migration2(),
|
||||||
Migration3(),
|
Migration3(),
|
||||||
Migration4(),
|
Migration4(),
|
||||||
|
Migration5(),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,17 @@ import android.content.Context
|
|||||||
import de.mm20.launcher2.search.SearchFilters
|
import de.mm20.launcher2.search.SearchFilters
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LauncherSettingsData internal constructor(
|
data class LauncherSettingsData internal constructor(
|
||||||
val schemaVersion: Int = 3,
|
val schemaVersion: Int = 5,
|
||||||
|
|
||||||
val uiColorScheme: ColorScheme = ColorScheme.System,
|
val uiColorScheme: ColorScheme = ColorScheme.System,
|
||||||
val uiTheme: ThemeDescriptor = ThemeDescriptor.Default,
|
val uiTheme: ThemeDescriptor = ThemeDescriptor.Default,
|
||||||
val uiCompatModeColors: Boolean = false,
|
val uiCompatModeColors: Boolean = false,
|
||||||
val uiFont: Font = Font.Outfit,
|
val uiFont: Font = Font.Outfit,
|
||||||
|
@Deprecated("No longer in use, only used for migration")
|
||||||
val uiBaseLayout: BaseLayout = BaseLayout.PullDown,
|
val uiBaseLayout: BaseLayout = BaseLayout.PullDown,
|
||||||
val uiOrientation: ScreenOrientation = ScreenOrientation.Auto,
|
val uiOrientation: ScreenOrientation = ScreenOrientation.Auto,
|
||||||
|
|
||||||
@ -39,10 +41,12 @@ data class LauncherSettingsData internal constructor(
|
|||||||
val clockWidgetBatteryPart: Boolean = true,
|
val clockWidgetBatteryPart: Boolean = true,
|
||||||
val clockWidgetMusicPart: Boolean = true,
|
val clockWidgetMusicPart: Boolean = true,
|
||||||
val clockWidgetDatePart: Boolean = true,
|
val clockWidgetDatePart: Boolean = true,
|
||||||
|
@Deprecated("Use homeScreenWidgets")
|
||||||
val clockWidgetFillHeight: Boolean = true,
|
val clockWidgetFillHeight: Boolean = true,
|
||||||
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,
|
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,
|
||||||
|
|
||||||
val homeScreenDock: Boolean = false,
|
val homeScreenDock: Boolean = false,
|
||||||
|
val homeScreenWidgets: Boolean = false,
|
||||||
|
|
||||||
val favoritesEnabled: Boolean = true,
|
val favoritesEnabled: Boolean = true,
|
||||||
val favoritesFrequentlyUsed: Boolean = true,
|
val favoritesFrequentlyUsed: Boolean = true,
|
||||||
@ -123,9 +127,10 @@ data class LauncherSettingsData internal constructor(
|
|||||||
|
|
||||||
val widgetsEditButton: Boolean = true,
|
val widgetsEditButton: Boolean = true,
|
||||||
|
|
||||||
val gesturesSwipeDown: GestureAction = GestureAction.Notifications,
|
val gesturesSwipeDown: GestureAction = GestureAction.Search,
|
||||||
val gesturesSwipeLeft: GestureAction = GestureAction.NoAction,
|
val gesturesSwipeLeft: GestureAction = GestureAction.NoAction,
|
||||||
val gesturesSwipeRight: GestureAction = GestureAction.NoAction,
|
val gesturesSwipeRight: GestureAction = GestureAction.NoAction,
|
||||||
|
val gesturesSwipeUp: GestureAction = GestureAction.Widgets,
|
||||||
val gesturesDoubleTap: GestureAction = GestureAction.ScreenLock,
|
val gesturesDoubleTap: GestureAction = GestureAction.ScreenLock,
|
||||||
val gesturesLongPress: GestureAction = GestureAction.NoAction,
|
val gesturesLongPress: GestureAction = GestureAction.NoAction,
|
||||||
val gesturesHomeButton: GestureAction = GestureAction.NoAction,
|
val gesturesHomeButton: GestureAction = GestureAction.NoAction,
|
||||||
@ -366,6 +371,10 @@ sealed interface GestureAction {
|
|||||||
@SerialName("search")
|
@SerialName("search")
|
||||||
data object Search : GestureAction
|
data object Search : GestureAction
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("widgets")
|
||||||
|
data object Widgets : GestureAction
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("power_menu")
|
@SerialName("power_menu")
|
||||||
data object PowerMenu : GestureAction
|
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
|
val fillHeight
|
||||||
get() = launcherDataStore.data.map { it.clockWidgetFillHeight }
|
get() = launcherDataStore.data.map { !it.homeScreenWidgets }
|
||||||
|
|
||||||
fun setFillHeight(fillHeight: Boolean) {
|
|
||||||
launcherDataStore.update {
|
|
||||||
it.copy(clockWidgetFillHeight = fillHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dock
|
val dock
|
||||||
get() = launcherDataStore.data.map { it.homeScreenDock }
|
get() = launcherDataStore.data.map { it.homeScreenDock }
|
||||||
|
|
||||||
fun setDock(dock: Boolean) {
|
|
||||||
launcherDataStore.update {
|
|
||||||
it.copy(homeScreenDock = dock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val alignment
|
val alignment
|
||||||
get() = launcherDataStore.data.map { it.clockWidgetAlignment }
|
get() = launcherDataStore.data.map { it.clockWidgetAlignment }
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ data class GestureSettingsData(
|
|||||||
val swipeDown: GestureAction,
|
val swipeDown: GestureAction,
|
||||||
val swipeLeft: GestureAction,
|
val swipeLeft: GestureAction,
|
||||||
val swipeRight: GestureAction,
|
val swipeRight: GestureAction,
|
||||||
|
val swipeUp: GestureAction,
|
||||||
val doubleTap: GestureAction,
|
val doubleTap: GestureAction,
|
||||||
val longPress: GestureAction,
|
val longPress: GestureAction,
|
||||||
val homeButton: GestureAction,
|
val homeButton: GestureAction,
|
||||||
@ -23,6 +24,7 @@ class GestureSettings internal constructor(
|
|||||||
swipeDown = it.gesturesSwipeDown,
|
swipeDown = it.gesturesSwipeDown,
|
||||||
swipeLeft = it.gesturesSwipeLeft,
|
swipeLeft = it.gesturesSwipeLeft,
|
||||||
swipeRight = it.gesturesSwipeRight,
|
swipeRight = it.gesturesSwipeRight,
|
||||||
|
swipeUp = it.gesturesSwipeUp,
|
||||||
doubleTap = it.gesturesDoubleTap,
|
doubleTap = it.gesturesDoubleTap,
|
||||||
longPress = it.gesturesLongPress,
|
longPress = it.gesturesLongPress,
|
||||||
homeButton = it.gesturesHomeButton,
|
homeButton = it.gesturesHomeButton,
|
||||||
@ -38,6 +40,9 @@ class GestureSettings internal constructor(
|
|||||||
val swipeRight: Flow<GestureAction> = dataStore.data.map { it.gesturesSwipeRight }
|
val swipeRight: Flow<GestureAction> = dataStore.data.map { it.gesturesSwipeRight }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
||||||
|
val swipeUp: Flow<GestureAction> = dataStore.data.map { it.gesturesSwipeUp }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
val doubleTap: Flow<GestureAction> = dataStore.data.map { it.gesturesDoubleTap }
|
val doubleTap: Flow<GestureAction> = dataStore.data.map { it.gesturesDoubleTap }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
||||||
@ -65,6 +70,12 @@ class GestureSettings internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSwipeUp(action: GestureAction) {
|
||||||
|
dataStore.update {
|
||||||
|
it.copy(gesturesSwipeUp = action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setDoubleTap(action: GestureAction) {
|
fun setDoubleTap(action: GestureAction) {
|
||||||
dataStore.update {
|
dataStore.update {
|
||||||
it.copy(gesturesDoubleTap = action)
|
it.copy(gesturesDoubleTap = action)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package de.mm20.launcher2.preferences.ui
|
package de.mm20.launcher2.preferences.ui
|
||||||
|
|
||||||
import de.mm20.launcher2.preferences.BaseLayout
|
|
||||||
import de.mm20.launcher2.preferences.ColorScheme
|
import de.mm20.launcher2.preferences.ColorScheme
|
||||||
import de.mm20.launcher2.preferences.Font
|
import de.mm20.launcher2.preferences.Font
|
||||||
import de.mm20.launcher2.preferences.IconShape
|
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
|
val clockFillScreen
|
||||||
get() = launcherDataStore.data.map {
|
get() = launcherDataStore.data.map {
|
||||||
it.clockWidgetFillHeight
|
it.homeScreenWidgets
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
val searchBarStyle
|
val searchBarStyle
|
||||||
@ -347,6 +335,23 @@ class UiSettings internal constructor(
|
|||||||
it.homeScreenDock
|
it.homeScreenDock
|
||||||
}.distinctUntilChanged()
|
}.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
|
val widgetEditButton
|
||||||
get() = launcherDataStore.data.map {
|
get() = launcherDataStore.data.map {
|
||||||
it.widgetsEditButton
|
it.widgetsEditButton
|
||||||
|
|||||||
@ -19,8 +19,8 @@ kotlinx-serialization = "1.8.1"
|
|||||||
|
|
||||||
jetbrains-markdown = "0.7.3"
|
jetbrains-markdown = "0.7.3"
|
||||||
|
|
||||||
androidx-compose = "1.9.0-alpha02"
|
androidx-compose = "1.9.0-alpha03"
|
||||||
androidx-compose-material3 = "1.4.0-alpha14"
|
androidx-compose-material3 = "1.4.0-alpha15"
|
||||||
androidx-compose-materialicons = "1.7.8"
|
androidx-compose-materialicons = "1.7.8"
|
||||||
androidx-lifecycle = "2.9.0"
|
androidx-lifecycle = "2.9.0"
|
||||||
androidx-core = "1.16.0"
|
androidx-core = "1.16.0"
|
||||||
@ -37,6 +37,7 @@ androidx-constraint-layout = "1.1.1"
|
|||||||
androidx-emojipicker = "1.5.0"
|
androidx-emojipicker = "1.5.0"
|
||||||
|
|
||||||
accompanist = "0.36.0"
|
accompanist = "0.36.0"
|
||||||
|
haze = "1.6.0"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
koin = "4.0.4"
|
koin = "4.0.4"
|
||||||
retrofit = "2.11.0"
|
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-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-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }
|
||||||
accompanist-navigationanimation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", 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-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "androidx-constraint-layout" }
|
||||||
androidx-transition = { group = "androidx.transition", name = "transition", version = "1.6.0" }
|
androidx-transition = { group = "androidx.transition", name = "transition", version = "1.6.0" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user