Redesign gesture settings

This commit is contained in:
MM20
2026-04-06 12:07:29 +02:00
parent b07773a8cc
commit b9c227be23
18 changed files with 826 additions and 466 deletions

View File

@@ -1,6 +1,9 @@
package de.mm20.launcher2.ui.common package de.mm20.launcher2.ui.common
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@@ -8,10 +11,11 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.DockedSearchBar import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -34,128 +38,153 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.animateShapeAsState
import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.ktx.toPixels
@Composable @Composable
fun SearchablePicker( fun SearchablePicker(
expanded: Boolean, modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
value: SavableSearchable?, value: SavableSearchable?,
onValueChanged: (SavableSearchable?) -> Unit, onValueChanged: (SavableSearchable?) -> Unit,
onDismissRequest: () -> Unit,
) { ) {
DismissableBottomSheet(expanded = expanded, onDismissRequest = onDismissRequest) { val viewModel: SearchablePickerVM = viewModel()
val viewModel: SearchablePickerVM = viewModel()
val colorSurface = MaterialTheme.colorScheme.surfaceContainerLow val colorSurface = MaterialTheme.colorScheme.surfaceContainerLow
LazyColumn( LazyColumn(
modifier = Modifier modifier = modifier,
.fillMaxSize(), contentPadding = contentPadding,
contentPadding = PaddingValues( verticalArrangement = Arrangement.spacedBy(1.dp)
start = 16.dp, ) {
end = 16.dp, stickyHeader {
bottom = 16.dp + WindowInsets.navigationBars.asPaddingValues() DockedSearchBar(
.calculateBottomPadding(), modifier = Modifier
top = 16.dp, .fillMaxWidth()
) .drawBehind {
) { drawRect(
stickyHeader { brush = Brush.verticalGradient(
DockedSearchBar( 0.5f to colorSurface,
modifier = Modifier 0.5f to colorSurface.copy(alpha = 0f),
.fillMaxWidth()
.drawBehind {
drawRect(
brush = Brush.verticalGradient(
0.5f to colorSurface,
0.5f to colorSurface.copy(alpha = 0f),
)
) )
}
.padding(bottom = 16.dp),
expanded = false,
onExpandedChange = {},
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painterResource(R.drawable.search_24px),
contentDescription = null
)
},
trailingIcon = {
if (viewModel.searchQuery.isNotEmpty()) {
IconButton(
onClick = {
viewModel.onSearchQueryChanged("")
}) {
Icon(painterResource(R.drawable.close_24px), null)
}
}
},
onSearch = {},
expanded = false,
onExpandedChange = {},
query = viewModel.searchQuery,
onQueryChange = {
viewModel.onSearchQueryChanged(it)
},
placeholder = {
Text(stringResource(R.string.search_bar_placeholder))
},
) )
} }
) { .padding(bottom = 16.dp),
} expanded = false,
} onExpandedChange = {},
items(viewModel.items) { inputField = {
val iconSize = 32.dp.toPixels() SearchBarDefaults.InputField(
val icon by remember(it.key) { leadingIcon = {
viewModel.getIcon(
it,
iconSize.toInt()
)
}.collectAsStateWithLifecycle(null)
val selected = it.key == value?.key
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
shape = MaterialTheme.shapes.small,
border = BorderStroke(
1.dp,
if (selected) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.outline
),
color = if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface,
onClick = { onValueChanged(it) }) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
ShapedLauncherIcon(
icon = { icon },
size = 32.dp,
)
Text(
text = it.label,
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (selected) {
Icon( Icon(
painterResource(R.drawable.check_circle_24px_filled), painterResource(R.drawable.search_24px),
contentDescription = null, contentDescription = null
tint = MaterialTheme.colorScheme.secondary,
) )
} },
trailingIcon = {
if (viewModel.searchQuery.isNotEmpty()) {
IconButton(
modifier = Modifier.offset(16.dp),
onClick = {
viewModel.onSearchQueryChanged("")
}) {
Icon(painterResource(R.drawable.close_24px), null)
}
}
},
onSearch = {},
expanded = false,
onExpandedChange = {},
query = viewModel.searchQuery,
onQueryChange = {
viewModel.onSearchQueryChanged(it)
},
placeholder = {
Text(stringResource(R.string.search_bar_placeholder))
},
)
}
) {
}
}
itemsIndexed(viewModel.items) { i, it ->
val iconSize = 32.dp.toPixels()
val icon by remember(it.key) {
viewModel.getIcon(
it,
iconSize.toInt()
)
}.collectAsStateWithLifecycle(null)
val badge by remember(it.key) {
viewModel.getBadge(it)
}.collectAsStateWithLifecycle(null)
val selected = it.key == value?.key
val transition = updateTransition(selected)
val background by transition.animateColor {
if (it) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surfaceBright
}
val foreground by transition.animateColor {
if (it) MaterialTheme.colorScheme.onSecondaryContainer
else MaterialTheme.colorScheme.onSurface
}
val md = MaterialTheme.shapes.medium
val xs = MaterialTheme.shapes.extraSmall
val shape by animateShapeAsState(
if (selected || viewModel.items.size == 1) md
else when (i) {
0 -> md.copy(bottomStart = xs.bottomStart, bottomEnd = xs.bottomEnd)
viewModel.items.lastIndex -> md.copy(topStart = xs.topStart, topEnd = xs.topEnd)
else -> xs
}
)
Surface(
modifier = Modifier
.fillMaxWidth(),
shape = shape,
color = background,
contentColor = foreground,
onClick = { onValueChanged(it) }) {
Row(
modifier = Modifier
.padding(
start = 8.dp,
end = if (selected) 8.dp else 16.dp,
),
verticalAlignment = Alignment.CenterVertically
) {
ShapedLauncherIcon(
modifier = Modifier.padding(horizontal = 10.dp),
icon = { icon },
badge = { badge },
size = 36.dp,
)
Text(
text = it.label,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (selected) {
Icon(
painterResource(R.drawable.check_24px),
contentDescription = null,
modifier = Modifier.padding(16.dp),
tint = foreground,
)
} }
} }
} }
} }
} }
} }

View File

@@ -5,6 +5,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeService
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
@@ -24,6 +26,7 @@ class SearchablePickerVM : ViewModel(), KoinComponent {
private val searchService: SearchService by inject() private val searchService: SearchService by inject()
private val iconService: IconService by inject() private val iconService: IconService by inject()
private val badgeService: BadgeService by inject()
var searchQuery by mutableStateOf("") var searchQuery by mutableStateOf("")
@@ -54,4 +57,10 @@ class SearchablePickerVM : ViewModel(), KoinComponent {
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> { fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {
return iconService.getIcon(searchable, size) return iconService.getIcon(searchable, size)
} }
fun getBadge(searchable: SavableSearchable): Flow<Badge?> {
return badgeService.getBadge(searchable)
}
} }

View File

@@ -41,6 +41,7 @@ import de.mm20.launcher2.preferences.GestureAction
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.WidgetScreenTarget
import de.mm20.launcher2.search.SavableSearchable 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
@@ -289,7 +290,7 @@ abstract class SharedLauncherActivity(
) )
is GestureAction.Widgets -> is GestureAction.Widgets ->
if (widgetsOnHomeScreen == true) { if (widgetsOnHomeScreen == true && action.target == WidgetScreenTarget.Default) {
null null
} else { } else {
ScaffoldGesture( ScaffoldGesture(

View File

@@ -125,7 +125,7 @@ internal object ClockAndWidgetsHomeComponent : ScaffoldComponent() {
scope.launch { state.lock(hideSearchBar = true) } scope.launch { state.lock(hideSearchBar = true) }
editMode = it editMode = it
}, },
parentId = WidgetScreenTarget.Default.scopeId.toString(), parentId = WidgetScreenTarget.Default.id.toString(),
) )
} }
if (editMode) { if (editMode) {

View File

@@ -1,16 +1,13 @@
package de.mm20.launcher2.ui.launcher.scaffold package de.mm20.launcher2.ui.launcher.scaffold
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -38,8 +35,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
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.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
@@ -119,7 +114,7 @@ internal class WidgetsComponent(
scope.launch { state.lock(hideSearchBar = true) } scope.launch { state.lock(hideSearchBar = true) }
editMode = it editMode = it
}, },
parentId = target.scopeId.toString(), parentId = target.id.toString(),
) )
} }
if (editMode) { if (editMode) {

View File

@@ -0,0 +1,428 @@
package de.mm20.launcher2.ui.settings.gestures
import android.content.res.Resources
import android.icu.text.ListFormatter
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.WidgetScreenTarget
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.SearchablePicker
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.ktx.animateShapeAsState
import de.mm20.launcher2.ui.ktx.toPixels
import kotlinx.coroutines.flow.combine
import org.koin.compose.koinInject
import kotlin.reflect.KClass
@Composable
internal fun GesturePreference(
title: String,
@DrawableRes icon: Int,
value: GestureAction?,
onValueChanged: (GestureAction, SavableSearchable?) -> Unit,
options: Set<KClass<out GestureAction>>,
shortcutOptions: List<SavableSearchable>,
widgetOptions: List<WidgetPageOption>,
) {
var showSheet by remember { mutableStateOf(false) }
val value = if (value is GestureAction.Widgets && widgetOptions.none { it.id == value.target }) {
GestureAction.NoAction
} else {
value
}
val iconService: IconService = koinInject()
val iconSize = 24.dp.toPixels().toInt()
val shortcut = remember(shortcutOptions, value) {
if (value !is GestureAction.Launch) {
null
} else {
shortcutOptions.find { value.key == it.key }
}
}
val icons by remember(shortcutOptions) {
combine(shortcutOptions.map {
iconService.getIcon(it, iconSize)
}) { it.toList() }
}.collectAsStateWithLifecycle(emptyList())
Preference(
title = title,
summary = getActionLabel(LocalResources.current, value, shortcutOptions),
icon = icon,
onClick = {
showSheet = true
},
)
DismissableBottomSheet(
expanded = showSheet,
onDismissRequest = { showSheet = false }
) {
val contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
)
var showShortcutPicker by remember { mutableStateOf(false) }
AnimatedContent(
showShortcutPicker
) { shortcutPicker ->
if (shortcutPicker) {
SearchablePicker(
value = shortcut,
onValueChanged = {
onValueChanged(GestureAction.Launch(it?.key), it)
showSheet = false
},
modifier = Modifier.fillMaxSize(),
contentPadding = contentPadding,
)
} else {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = contentPadding,
) {
item {
PreferenceCategory {
GestureItem(
title = stringResource(R.string.gesture_action_none),
icon = R.drawable.close_24px,
selected = value is GestureAction.NoAction,
onClick = {
onValueChanged(GestureAction.NoAction, null)
showSheet = false
}
)
}
}
item {
PreferenceCategory(
title = stringResource(R.string.gesture_action_category_launcher)
) {
if (options.contains(GestureAction.Search::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_open_search),
icon = R.drawable.search_24px,
selected = value is GestureAction.Search,
onClick = {
onValueChanged(GestureAction.Search, null)
showSheet = false
}
)
}
if (options.contains(GestureAction.Feed::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_feed),
icon = R.drawable.news_24px,
selected = value is GestureAction.Feed,
onClick = {
onValueChanged(GestureAction.Feed, null)
showSheet = false
}
)
}
if (options.contains(GestureAction.Widgets::class)) {
for (widget in widgetOptions) {
GestureItem(
title = if (widget.isNewPage) {
stringResource(R.string.gesture_action_widgets_new)
} else {
getActionLabel(
LocalResources.current,
GestureAction.Widgets(widget.id),
shortcutOptions
)
},
icon = if (widget.isNewPage) R.drawable.widgets_add_24px else R.drawable.widgets_24px,
selected = value is GestureAction.Widgets && value.target == widget.id,
onClick = {
onValueChanged(GestureAction.Widgets(widget.id), null)
showSheet = false
},
summary = if (widget.isNewPage) {
null
} else if (widget.widgets.isEmpty()) {
stringResource(R.string.gesture_action_widgets_empty)
} else {
ListFormatter.getInstance().format(
widget.widgets.map { it.getLabel(LocalContext.current) }
)
}
)
}
}
}
}
item {
PreferenceCategory(
title = stringResource(R.string.gesture_action_category_system)
) {
if (options.contains(GestureAction.Notifications::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_notifications),
icon = R.drawable.notifications_24px,
selected = value is GestureAction.Notifications,
onClick = {
onValueChanged(GestureAction.Notifications, null)
showSheet = false
}
)
}
if (options.contains(GestureAction.QuickSettings::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_quick_settings),
icon = R.drawable.settings_24px,
selected = value is GestureAction.QuickSettings,
onClick = {
onValueChanged(GestureAction.QuickSettings, null)
showSheet = false
}
)
}
if (options.contains(GestureAction.ScreenLock::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_lock_screen),
icon = R.drawable.lock_24px,
selected = value is GestureAction.ScreenLock,
onClick = {
onValueChanged(GestureAction.ScreenLock, null)
showSheet = false
}
)
}
if (options.contains(GestureAction.PowerMenu::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_power_menu),
icon = R.drawable.power_settings_new_24px,
selected = value is GestureAction.PowerMenu,
onClick = {
onValueChanged(GestureAction.PowerMenu, null)
showSheet = false
}
)
}
if (options.contains(GestureAction.Recents::class)) {
GestureItem(
title = stringResource(R.string.gesture_action_recents),
icon = R.drawable.amp_stories_24px,
selected = value is GestureAction.Recents,
onClick = {
onValueChanged(GestureAction.Recents, null)
showSheet = false
}
)
}
}
}
item {
PreferenceCategory(
title = stringResource(R.string.gesture_action_category_apps)
) {
for ((i, shortcut) in shortcutOptions.withIndex()) {
GestureItem(
title = shortcut.labelOverride ?: shortcut.label,
icon = R.drawable.android_24px,
shortcutIcon = icons.getOrNull(i),
selected = value is GestureAction.Launch && value.key == shortcut.key,
onClick = {
onValueChanged(GestureAction.Launch(shortcut.key), shortcut)
showSheet = false
}
)
}
GestureItem(
title = stringResource(R.string.gesture_action_launch_select_app),
icon = R.drawable.action_key_24px,
selected = false,
onClick = {
showShortcutPicker = true
}
)
}
}
}
}
}
}
}
@Composable
private fun GestureItem(
title: String,
summary: String? = null,
@DrawableRes icon: Int,
shortcutIcon: LauncherIcon? = null,
selected: Boolean,
onClick: () -> Unit,
) {
val transition = updateTransition(selected)
val backgroundColor by transition.animateColor {
if (it) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.surfaceBright
}
val textColor by transition.animateColor {
if (it) MaterialTheme.colorScheme.onSecondaryContainer
else MaterialTheme.colorScheme.onSurface
}
val iconColor by transition.animateColor {
if (it) MaterialTheme.colorScheme.onSecondaryContainer
else MaterialTheme.colorScheme.primary
}
val shape by animateShapeAsState(
if (selected) MaterialTheme.shapes.medium else MaterialTheme.shapes.extraSmall
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(shape)
.clickable(onClick = onClick)
.background(backgroundColor)
.padding(
start = 8.dp,
end = if (selected) 8.dp else 16.dp,
)
) {
Box(
modifier = Modifier
.width(56.dp)
.padding(end = 8.dp),
contentAlignment = Alignment.Center
) {
if (shortcutIcon != null) {
ShapedLauncherIcon(
size = 36.dp,
icon = { shortcutIcon },
)
} else {
Icon(
painterResource(icon),
contentDescription = null,
tint = iconColor
)
}
}
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp),
) {
Text(
text = title,
color = textColor,
style = MaterialTheme.typography.titleMedium,
)
if (summary != null) {
Text(
text = summary,
color = textColor,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
if (selected) {
Box(
modifier = Modifier
.width(56.dp)
.padding(end = 8.dp),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(R.drawable.check_24px),
contentDescription = null,
tint = iconColor
)
}
}
}
}
private fun getActionLabel(
resources: Resources,
action: GestureAction?,
shortcutOptions: List<SavableSearchable>
): String {
return when (action) {
GestureAction.Feed -> resources.getString(R.string.gesture_action_feed)
is GestureAction.Launch -> {
shortcutOptions.find { it.key == action.key }
?.let { it.labelOverride ?: it.label }
?: resources.getString(R.string.gesture_action_launch_app)
}
GestureAction.Notifications -> resources.getString(R.string.gesture_action_notifications)
GestureAction.PowerMenu -> resources.getString(R.string.gesture_action_power_menu)
GestureAction.QuickSettings -> resources.getString(R.string.gesture_action_quick_settings)
GestureAction.Recents -> resources.getString(R.string.gesture_action_recents)
GestureAction.ScreenLock -> resources.getString(R.string.gesture_action_lock_screen)
GestureAction.Search -> resources.getString(R.string.gesture_action_open_search)
is GestureAction.Widgets -> {
when (action.target) {
WidgetScreenTarget.Widgets1 -> resources.getString(R.string.gesture_action_widgets)
WidgetScreenTarget.Widgets2 -> resources.getString(
R.string.gesture_action_widgets_indexed,
2
)
WidgetScreenTarget.Widgets3 -> resources.getString(
R.string.gesture_action_widgets_indexed,
3
)
WidgetScreenTarget.Widgets4 -> resources.getString(
R.string.gesture_action_widgets_indexed,
4
)
}
}
else -> resources.getString(R.string.gesture_action_none)
}
}

View File

@@ -9,7 +9,10 @@ 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.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
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 +23,7 @@ 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -49,51 +53,33 @@ fun GestureSettingsScreen() {
val viewModel: GestureSettingsScreenVM = viewModel() val viewModel: GestureSettingsScreenVM = viewModel()
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null) val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
val allowWidgetGesture by viewModel.allowWidgetGesture.collectAsStateWithLifecycle(null)
val widgetScreenCount by viewModel.widgetScreenCount.collectAsStateWithLifecycle(1)
val options = buildList { val options = buildSet {
add(stringResource(R.string.gesture_action_none) to GestureAction.NoAction) add(GestureAction.NoAction::class)
add(stringResource(R.string.gesture_action_notifications) to GestureAction.Notifications) add(GestureAction.Notifications::class)
add(stringResource(R.string.gesture_action_quick_settings) to GestureAction.QuickSettings) add(GestureAction.QuickSettings::class)
if (isAtLeastApiLevel(28)) add(stringResource(R.string.gesture_action_lock_screen) to GestureAction.ScreenLock) if (isAtLeastApiLevel(28)) add(GestureAction.ScreenLock::class)
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents) add(GestureAction.Recents::class)
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu) add(GestureAction.PowerMenu::class)
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search) add(GestureAction.Search::class)
if (allowWidgetGesture == true) { add(GestureAction.Widgets::class)
// Dynamically add widget screen targets based on user configuration add(GestureAction.Launch::class)
WidgetScreenTarget.getAvailableTargets(widgetScreenCount)
.forEachIndexed { index, target ->
val label =
if (widgetScreenCount == 1) {
stringResource(R.string.gesture_action_widgets)
} else {
stringResource(R.string.gesture_action_widgets_indexed, index + 1)
}
add(label to GestureAction.Widgets(target))
}
}
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
} }
val shortcutOptions by viewModel.shortcutOptions.collectAsStateWithLifecycle(emptyList())
val widgetOptions by viewModel.widgetOptions.collectAsStateWithLifecycle(emptyList())
val optionsWithFeed = val optionsWithFeed =
if (FeatureFlags.feed) { if (FeatureFlags.feed) {
options + (stringResource(R.string.gesture_action_feed) to GestureAction.Feed) options + GestureAction.Feed::class
} else options } else options
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 { item {
val appIconSize = 32.dp.toPixels()
PreferenceCategory { PreferenceCategory {
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null) val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null)
val swipeDownApp by viewModel.swipeDownApp.collectAsState(null)
val swipeDownAppIcon by remember(swipeDownApp?.key) {
viewModel.getIcon(swipeDownApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(swipeDown), locked = hasPermission == false && requiresAccessibilityService(swipeDown),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -103,19 +89,14 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_swipe_down), title = stringResource(R.string.preference_gesture_swipe_down),
icon = R.drawable.swipe_down_alt_24px, icon = R.drawable.swipe_down_alt_24px,
value = swipeDown, value = swipeDown,
onValueChanged = { viewModel.setSwipeDown(it) }, onValueChanged = viewModel::setSwipeDown,
options = options, options = options,
app = swipeDownApp, shortcutOptions = shortcutOptions,
appIcon = swipeDownAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setSwipeDownApp(it) },
) )
} }
val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null) val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null)
val swipeLeftApp by viewModel.swipeLeftApp.collectAsState(null)
val swipeLeftAppIcon by remember(swipeLeftApp?.key) {
viewModel.getIcon(swipeLeftApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(swipeLeft), locked = hasPermission == false && requiresAccessibilityService(swipeLeft),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -125,19 +106,14 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_swipe_left), title = stringResource(R.string.preference_gesture_swipe_left),
icon = R.drawable.swipe_left_alt_24px, icon = R.drawable.swipe_left_alt_24px,
value = swipeLeft, value = swipeLeft,
onValueChanged = { viewModel.setSwipeLeft(it) }, onValueChanged = viewModel::setSwipeLeft,
options = options, options = options,
app = swipeLeftApp, shortcutOptions = shortcutOptions,
appIcon = swipeLeftAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setSwipeLeftApp(it) },
) )
} }
val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null) val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null)
val swipeRightApp by viewModel.swipeRightApp.collectAsState(null)
val swipeRightAppIcon by remember(swipeRightApp?.key) {
viewModel.getIcon(swipeRightApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(swipeRight), locked = hasPermission == false && requiresAccessibilityService(swipeRight),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -147,19 +123,14 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_swipe_right), title = stringResource(R.string.preference_gesture_swipe_right),
icon = R.drawable.swipe_right_alt_24px, icon = R.drawable.swipe_right_alt_24px,
value = swipeRight, value = swipeRight,
onValueChanged = { viewModel.setSwipeRight(it) }, onValueChanged = viewModel::setSwipeRight,
options = optionsWithFeed, options = optionsWithFeed,
app = swipeRightApp, shortcutOptions = shortcutOptions,
appIcon = swipeRightAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setSwipeRightApp(it) },
) )
} }
val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null) val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null)
val swipeUpApp by viewModel.swipeUpApp.collectAsState(null)
val swipeUpAppIcon by remember(swipeUpApp?.key) {
viewModel.getIcon(swipeUpApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(swipeUp), locked = hasPermission == false && requiresAccessibilityService(swipeUp),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -169,19 +140,14 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_swipe_up), title = stringResource(R.string.preference_gesture_swipe_up),
icon = R.drawable.swipe_up_alt_24px, icon = R.drawable.swipe_up_alt_24px,
value = swipeUp, value = swipeUp,
onValueChanged = { viewModel.setSwipeUp(it) }, onValueChanged = viewModel::setSwipeUp,
options = options, options = options,
app = swipeUpApp, shortcutOptions = shortcutOptions,
appIcon = swipeUpAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setSwipeUpApp(it) },
) )
} }
val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null) val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null)
val doubleTapApp by viewModel.doubleTapApp.collectAsState(null)
val doubleTapAppIcon by remember(doubleTapApp?.key) {
viewModel.getIcon(doubleTapApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(doubleTap), locked = hasPermission == false && requiresAccessibilityService(doubleTap),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -191,19 +157,14 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_double_tap), title = stringResource(R.string.preference_gesture_double_tap),
icon = R.drawable.adjust_24px, icon = R.drawable.adjust_24px,
value = doubleTap, value = doubleTap,
onValueChanged = { viewModel.setDoubleTap(it) }, onValueChanged = viewModel::setDoubleTap,
options = options, options = options,
app = doubleTapApp, shortcutOptions = shortcutOptions,
appIcon = doubleTapAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setDoubleTapApp(it) },
) )
} }
val longPress by viewModel.longPress.collectAsStateWithLifecycle(null) val longPress by viewModel.longPress.collectAsStateWithLifecycle(null)
val longPressApp by viewModel.longPressApp.collectAsState(null)
val longPressAppIcon by remember(longPressApp?.key) {
viewModel.getIcon(longPressApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(longPress), locked = hasPermission == false && requiresAccessibilityService(longPress),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -213,18 +174,13 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_long_press), title = stringResource(R.string.preference_gesture_long_press),
icon = R.drawable.circle_24px, icon = R.drawable.circle_24px,
value = longPress, value = longPress,
onValueChanged = { viewModel.setLongPress(it) }, onValueChanged = viewModel::setLongPress,
options = options, options = options,
app = longPressApp, shortcutOptions = shortcutOptions,
appIcon = longPressAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setLongPressApp(it) },
) )
} }
val homeButton by viewModel.homeButton.collectAsStateWithLifecycle(null) val homeButton by viewModel.homeButton.collectAsStateWithLifecycle(null)
val homeButtonApp by viewModel.homeButtonApp.collectAsState(null)
val homeButtonAppIcon by remember(homeButtonApp?.key) {
viewModel.getIcon(homeButtonApp, appIconSize.toInt())
}.collectAsState(null)
GuardedPreference( GuardedPreference(
locked = hasPermission == false && requiresAccessibilityService(homeButton), locked = hasPermission == false && requiresAccessibilityService(homeButton),
description = stringResource(R.string.missing_permission_accessibility_gesture_settings), description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
@@ -234,11 +190,10 @@ fun GestureSettingsScreen() {
title = stringResource(R.string.preference_gesture_home_button), title = stringResource(R.string.preference_gesture_home_button),
icon = R.drawable.home_24px, icon = R.drawable.home_24px,
value = homeButton, value = homeButton,
onValueChanged = { viewModel.setHomeButton(it) }, onValueChanged = viewModel::setHomeButton,
options = options, options = options,
app = homeButtonApp, shortcutOptions = shortcutOptions,
appIcon = homeButtonAppIcon, widgetOptions = widgetOptions,
onAppChanged = { viewModel.setHomeButtonApp(it) },
) )
} }
} }
@@ -257,75 +212,88 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
} }
} }
@Composable //@Composable
fun GesturePreference( //fun GesturePreference(
title: String, // title: String,
@DrawableRes icon: Int, // @DrawableRes icon: Int,
value: GestureAction?, // value: GestureAction?,
onValueChanged: (GestureAction) -> Unit, // onValueChanged: (GestureAction) -> Unit,
options: List<Pair<String, GestureAction>>, // options: List<Pair<String, GestureAction>>,
app: SavableSearchable?, // app: SavableSearchable?,
appIcon: LauncherIcon?, // appIcon: LauncherIcon?,
onAppChanged: (SavableSearchable?) -> Unit, // onAppChanged: (SavableSearchable?) -> Unit,
) { //) {
var showAppPicker by remember { mutableStateOf(false) } // var showAppPicker by remember { mutableStateOf(false) }
Row( // Row(
verticalAlignment = (Alignment.CenterVertically), // verticalAlignment = (Alignment.CenterVertically),
) { // ) {
Box( // Box(
modifier = Modifier.weight(1f), // modifier = Modifier.weight(1f),
) { // ) {
ListPreference( // ListPreference(
title = title, // title = title,
icon = icon, // icon = icon,
items = options, // items = options,
value = value, // value = value,
summary = options.find { option -> // summary = options.find { option ->
when { // when {
value is GestureAction.Widgets && option.second is GestureAction.Widgets -> { // value is GestureAction.Widgets && option.second is GestureAction.Widgets -> {
val valueTarget = value.target // val valueTarget = value.target
val optionTarget = (option.second as GestureAction.Widgets).target // val optionTarget = (option.second as GestureAction.Widgets).target
valueTarget == optionTarget // valueTarget == optionTarget
} // }
//
else -> value?.javaClass == option.second.javaClass // else -> value?.javaClass == option.second.javaClass
} // }
}?.first ?: stringResource(R.string.gesture_action_none), // }?.first ?: stringResource(R.string.gesture_action_none),
onValueChanged = { if (it != null) onValueChanged(it) }, // onValueChanged = { if (it != null) onValueChanged(it) },
) // )
} // }
//
if (value is GestureAction.Launch) { // if (value is GestureAction.Launch) {
Box( // Box(
modifier = Modifier // modifier = Modifier
.height(36.dp) // .height(36.dp)
.width(1.dp) // .width(1.dp)
.alpha(0.38f) // .background(MaterialTheme.colorScheme.outlineVariant),
.background(LocalContentColor.current), // )
) // Box(
Box( // modifier = Modifier
modifier = Modifier // .clickable { showAppPicker = true }
.clickable { showAppPicker = true } // .padding(12.dp),
.padding(12.dp), // ) {
) { // ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
ShapedLauncherIcon(size = 32.dp, icon = { appIcon }) // }
} // }
} // if (value is GestureAction.Widgets) {
} // Box(
// modifier = Modifier
if (value is GestureAction.Launch) { // .height(36.dp)
SearchablePicker( // .width(1.dp)
expanded = (showAppPicker || app == null), // .background(MaterialTheme.colorScheme.outlineVariant),
onDismissRequest = { // )
showAppPicker = false // IconButton(
if (app == null) onValueChanged(GestureAction.NoAction) // modifier = Modifier.padding(4.dp),
}, // onClick = {}
value = app, // ) {
onValueChanged = { // Icon(painterResource(R.drawable.tune_24px), "")
showAppPicker = false // }
onAppChanged(it) // }
}, // }
) //
} // if (value is GestureAction.Launch) {
// SearchablePicker(
} // expanded = (showAppPicker || app == null),
// onDismissRequest = {
// showAppPicker = false
// if (app == null) onValueChanged(GestureAction.NoAction)
// },
// value = app,
// onValueChanged = {
// showAppPicker = false
// onAppChanged(it)
// },
// )
// }
//
//}

View File

@@ -7,39 +7,36 @@ import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
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.BaseLayout
import de.mm20.launcher2.preferences.GestureAction import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.WidgetScreenTarget
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.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
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
class GestureSettingsScreenVM : ViewModel(), KoinComponent { internal class GestureSettingsScreenVM : ViewModel(), KoinComponent {
private val gestureSettings: GestureSettings by inject() private val gestureSettings: GestureSettings by inject()
private val uiSettings: UiSettings by inject() private val uiSettings: UiSettings by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val searchableRepository: SavableSearchableRepository by inject() private val searchableRepository: SavableSearchableRepository by inject()
private val iconService: IconService by inject() private val iconService: IconService by inject()
private val widgetRepository: WidgetRepository by inject()
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility) val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val allowWidgetGesture = uiSettings.homeScreenWidgets.map { it == false }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val widgetScreenCount = uiSettings.widgetScreenCount
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
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
@@ -55,131 +52,101 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val homeButton = gestureSettings.homeButton val homeButton = gestureSettings.homeButton
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setSwipeDown(action: GestureAction) { fun setSwipeDown(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setSwipeDown(action) gestureSettings.setSwipeDown(action)
} }
fun setSwipeLeft(action: GestureAction) { fun setSwipeLeft(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setSwipeLeft(action) gestureSettings.setSwipeLeft(action)
} }
fun setSwipeRight(action: GestureAction) { fun setSwipeRight(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setSwipeRight(action) gestureSettings.setSwipeRight(action)
} }
fun setSwipeUp(action: GestureAction) { fun setSwipeUp(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setSwipeUp(action) gestureSettings.setSwipeUp(action)
} }
fun setDoubleTap(action: GestureAction) { fun setDoubleTap(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setDoubleTap(action) gestureSettings.setDoubleTap(action)
} }
fun setLongPress(action: GestureAction) { fun setLongPress(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setLongPress(action) gestureSettings.setLongPress(action)
} }
fun setHomeButton(action: GestureAction) { fun setHomeButton(action: GestureAction, searchable: SavableSearchable?) {
saveShortcut(searchable)
gestureSettings.setHomeButton(action) gestureSettings.setHomeButton(action)
} }
val swipeLeftApp: Flow<SavableSearchable?> = swipeLeft private fun saveShortcut(searchable: SavableSearchable?) {
.flatMapLatest { searchable?.let { searchableRepository.insert(it) }
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 setSwipeLeftApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeLeft(GestureAction.Launch(searchable.key))
} }
val swipeRightApp: Flow<SavableSearchable?> = swipeRight val shortcutOptions: Flow<List<SavableSearchable>> = gestureSettings.flatMapLatest {
.flatMapLatest { val keys = listOfNotNull(
if (it !is GestureAction.Launch || it.key == null) flowOf(null) (it.swipeUp as? GestureAction.Launch)?.key,
else searchableRepository.getByKeys(listOf(it.key!!)).map { (it.swipeLeft as? GestureAction.Launch)?.key,
it.firstOrNull() (it.swipeRight as? GestureAction.Launch)?.key,
} (it.swipeDown as? GestureAction.Launch)?.key,
} (it.doubleTap as? GestureAction.Launch)?.key,
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) (it.homeButton as? GestureAction.Launch)?.key,
(it.longPress as? GestureAction.Launch)?.key,
fun setSwipeRightApp(searchable: SavableSearchable?) { )
searchable?.let { searchableRepository.insert(it) } ?: return searchableRepository.getByKeys(keys)
setSwipeRight(GestureAction.Launch(searchable.key))
} }
val swipeDownApp: Flow<SavableSearchable?> = swipeDown val widgetOptions: Flow<List<WidgetPageOption>> =
.flatMapLatest { combine(uiSettings.homeScreenWidgets, gestureSettings) { home, gestures ->
if (it !is GestureAction.Launch || it.key == null) flowOf(null) val options = mutableListOf<WidgetPageOption>()
else searchableRepository.getByKeys(listOf(it.key!!)).map { val usedTargets = setOfNotNull(
it.firstOrNull() (gestures.swipeUp as? GestureAction.Widgets)?.target,
(gestures.swipeRight as? GestureAction.Widgets)?.target,
(gestures.swipeLeft as? GestureAction.Widgets)?.target,
(gestures.swipeDown as? GestureAction.Widgets)?.target,
(gestures.doubleTap as? GestureAction.Widgets)?.target,
(gestures.longPress as? GestureAction.Widgets)?.target,
(gestures.homeButton as? GestureAction.Widgets)?.target,
)
// We want to provide at most one "new screen" option
var newScreenOptionAdded = false
for (target in WidgetScreenTarget.entries) {
// Never offer the default screen as an option when widgets on home is enabled
if (home && target == WidgetScreenTarget.Default) continue
val widgets = widgetRepository.get(
parent = target.id,
).first()
if (widgets.isNotEmpty() || usedTargets.contains(target)) {
options += WidgetPageOption(
widgets = widgets,
id = target,
isNewPage = false,
)
} else if (!newScreenOptionAdded) {
options += WidgetPageOption(
id = target,
widgets = emptyList(),
isNewPage = true,
)
newScreenOptionAdded = true
}
}
options.sortedWith { a, b ->
if (a.isNewPage) return@sortedWith 1
if (b.isNewPage) return@sortedWith -1
a.id.compareTo(b.id)
} }
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeDownApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeDown(GestureAction.Launch(searchable.key))
}
val swipeUpApp: Flow<SavableSearchable?> = swipeUp
.flatMapLatest {
if (it !is GestureAction.Launch || it.key == null) flowOf(null)
else searchableRepository.getByKeys(listOf(it.key!!)).map {
it.firstOrNull()
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeUpApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeUp(GestureAction.Launch(searchable.key))
}
val longPressApp: Flow<SavableSearchable?> = longPress
.flatMapLatest {
if (it !is GestureAction.Launch || it.key == null) flowOf(null)
else searchableRepository.getByKeys(listOf(it.key!!)).map {
it.firstOrNull()
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setLongPressApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setLongPress(GestureAction.Launch(searchable.key))
}
val doubleTapApp: Flow<SavableSearchable?> = doubleTap
.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 setDoubleTapApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setDoubleTap(GestureAction.Launch(searchable.key))
}
val homeButtonApp: Flow<SavableSearchable?> = homeButton
.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 setHomeButtonApp(searchable: SavableSearchable?) {
searchable?.let { searchableRepository.insert(it) } ?: return
setHomeButton(GestureAction.Launch(searchable.key))
}
fun requestPermission(context: AppCompatActivity) { fun requestPermission(context: AppCompatActivity) {
@@ -191,3 +158,9 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
return iconService.getIcon(searchable, size) return iconService.getIcon(searchable, size)
} }
} }
internal data class WidgetPageOption(
val id: WidgetScreenTarget,
val widgets: List<Widget>,
val isNewPage: Boolean,
)

View File

@@ -135,28 +135,6 @@ fun HomescreenSettingsScreen() {
onValueChanged = { onValueChanged = {
viewModel.setWidgetsOnHomeScreen(it) viewModel.setWidgetsOnHomeScreen(it)
}) })
AnimatedVisibility(widgetsOnHomeScreen == false) {
val widgetScreenCount by viewModel.widgetScreenCount.collectAsStateWithLifecycle(1)
Column {
SliderPreference(
title = stringResource(R.string.preference_widget_screen_count),
value = widgetScreenCount,
min = 1,
max = 4,
onValueChanged = {
viewModel.setWidgetScreenCount(it)
}
)
Text(
text = stringResource(R.string.preference_widget_screen_count_info),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
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),

View File

@@ -166,13 +166,6 @@ class HomescreenSettingsScreenVM(
uiSettings.setHomeScreenWidgets(widgetsOnHomeScreen) uiSettings.setHomeScreenWidgets(widgetsOnHomeScreen)
} }
val widgetScreenCount = uiSettings.widgetScreenCount
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
fun setWidgetScreenCount(count: Int) {
uiSettings.setWidgetScreenCount(count)
}
companion object : KoinComponent { companion object : KoinComponent {
val Factory = viewModelFactory { val Factory = viewModelFactory {
initializer { initializer {

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M660,820Q594,820 547,773Q500,726 500,660Q500,594 547,547Q594,500 660,500Q726,500 773,547Q820,594 820,660Q820,683 814,703.5Q808,724 797,742L892,836Q898,841 901,848.5Q904,856 904,864Q904,872 901,879Q898,886 892,892Q887,897 879.5,900Q872,903 864,903Q856,903 849,900Q842,897 836,892L741,798Q723,809 702.5,814.5Q682,820 660,820ZM220,820Q154,820 107,773Q60,726 60,660Q60,594 107,547Q154,500 220,500Q286,500 333,547Q380,594 380,660Q380,726 333,773Q286,820 220,820ZM220,740Q253,740 276.5,716.5Q300,693 300,660Q300,627 276.5,603.5Q253,580 220,580Q187,580 163.5,603.5Q140,627 140,660Q140,693 163.5,716.5Q187,740 220,740ZM660,740Q693,740 716.5,716.5Q740,693 740,660Q740,627 716.5,603.5Q693,580 660,580Q627,580 603.5,603.5Q580,627 580,660Q580,693 603.5,716.5Q627,740 660,740ZM220,380Q154,380 107,333Q60,286 60,220Q60,154 107,107Q154,60 220,60Q286,60 333,107Q380,154 380,220Q380,286 333,333Q286,380 220,380ZM660,380Q594,380 547,333Q500,286 500,220Q500,154 547,107Q594,60 660,60Q726,60 773,107Q820,154 820,220Q820,286 773,333Q726,380 660,380ZM220,300Q253,300 276.5,276.5Q300,253 300,220Q300,187 276.5,163.5Q253,140 220,140Q187,140 163.5,163.5Q140,187 140,220Q140,253 163.5,276.5Q187,300 220,300ZM660,300Q693,300 716.5,276.5Q740,253 740,220Q740,187 716.5,163.5Q693,140 660,140Q627,140 603.5,163.5Q580,187 580,220Q580,253 603.5,276.5Q627,300 660,300ZM220,660Q220,660 220,660Q220,660 220,660Q220,660 220,660Q220,660 220,660Q220,660 220,660Q220,660 220,660Q220,660 220,660Q220,660 220,660ZM220,220Q220,220 220,220Q220,220 220,220Q220,220 220,220Q220,220 220,220Q220,220 220,220Q220,220 220,220Q220,220 220,220Q220,220 220,220ZM660,220Q660,220 660,220Q660,220 660,220Q660,220 660,220Q660,220 660,220Q660,220 660,220Q660,220 660,220Q660,220 660,220Q660,220 660,220Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M320,800Q303,800 291.5,788.5Q280,777 280,760L280,200Q280,183 291.5,171.5Q303,160 320,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,760Q680,777 668.5,788.5Q657,800 640,800L320,800ZM120,680L120,279Q120,262 131.5,251Q143,240 160,240Q177,240 188.5,251.5Q200,263 200,280L200,681Q200,698 188.5,709Q177,720 160,720Q143,720 131.5,708.5Q120,697 120,680ZM760,680L760,279Q760,262 771.5,251Q783,240 800,240Q817,240 828.5,251.5Q840,263 840,280L840,681Q840,698 828.5,709Q817,720 800,720Q783,720 771.5,708.5Q760,697 760,680ZM360,720L600,720L600,240L360,240L360,720ZM360,720L360,240L360,240L360,720Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M638,492L468,322Q462,316 459.5,309Q457,302 457,294Q457,286 459.5,279Q462,272 468,266L638,96Q644,90 651,87.5Q658,85 666,85Q674,85 681,87.5Q688,90 694,96L864,266Q870,272 872.5,279Q875,286 875,294Q875,302 872.5,309Q870,316 864,322L694,492Q688,498 681,500.5Q674,503 666,503Q658,503 651,500.5Q644,498 638,492ZM120,400L120,160Q120,143 131.5,131.5Q143,120 160,120L400,120Q417,120 428.5,131.5Q440,143 440,160L440,400Q440,417 428.5,428.5Q417,440 400,440L160,440Q143,440 131.5,428.5Q120,417 120,400ZM120,800L120,560Q120,543 131.5,531.5Q143,520 160,520L400,520Q417,520 428.5,531.5Q440,543 440,560L440,800Q440,817 428.5,828.5Q417,840 400,840L160,840Q143,840 131.5,828.5Q120,817 120,800ZM200,360L360,360L360,200L200,200L200,360ZM667,408L780,295L667,182L554,295L667,408ZM200,760L360,760L360,600L200,600L200,760ZM360,360L360,360L360,360L360,360ZM554,295L554,295L554,295L554,295ZM360,600L360,600L360,600L360,600ZM600,600L600,600L600,600L600,600Z M680,840Q663,840 651.5,828.5Q640,817 640,800L640,720L559,720Q542,720 531,708.5Q520,697 520,680Q520,663 531.5,651.5Q543,640 560,640L640,640L640,559Q640,542 651.5,531Q663,520 680,520Q697,520 708.5,531.5Q720,543 720,560L720,640L801,640Q818,640 829,651.5Q840,663 840,680Q840,697 828.5,708.5Q817,720 800,720L720,720L720,801Q720,818 708.5,829Q697,840 680,840Z"/>
</vector>

View File

@@ -812,15 +812,21 @@
<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_category_launcher">Launcher pages</string>
<string name="gesture_action_category_system">System actions</string>
<string name="gesture_action_category_apps">Apps and shortcuts</string>
<string name="gesture_action_open_search">Search / app drawer</string>
<string name="gesture_action_widgets">Widgets</string> <string name="gesture_action_widgets">Widgets</string>
<string name="gesture_action_widgets_indexed">Widgets %1$d</string> <string name="gesture_action_widgets_new">New widgets page</string>
<string name="gesture_action_widgets_empty">Empty</string>
<string name="gesture_action_widgets_indexed">Widgets (%1$d)</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_launch_select_app">Select app or shortcut</string>
<string name="gesture_action_notifications">Notifications</string>
<string name="gesture_action_lock_screen">Turn off screen</string> <string name="gesture_action_lock_screen">Turn off screen</string>
<string name="gesture_action_quick_settings">Open quick settings</string> <string name="gesture_action_quick_settings">Quick settings</string>
<string name="gesture_action_power_menu">Show power menu</string> <string name="gesture_action_power_menu">Power menu</string>
<string name="gesture_action_recents">Open recent apps</string> <string name="gesture_action_recents">Recent apps</string>
<string name="gesture_action_feed">Feed</string> <string name="gesture_action_feed">Feed</string>
<string name="gesture_failed_message">You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason:</string> <string name="gesture_failed_message">You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason:</string>
<string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string> <string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string>

View File

@@ -18,7 +18,7 @@ enum class WidgetScreenTarget {
* Returns the UUID used as parent identifier for widget repository operations. * Returns the UUID used as parent identifier for widget repository operations.
* These are deterministic UUIDs generated using UUID v5 (name-based). * These are deterministic UUIDs generated using UUID v5 (name-based).
*/ */
val scopeId: UUID val id: UUID
get() = when (this) { get() = when (this) {
Widgets1 -> UUID.fromString("00000000-0000-0000-0000-000000000001") Widgets1 -> UUID.fromString("00000000-0000-0000-0000-000000000001")
Widgets2 -> UUID.fromString("00000000-0000-0000-0000-000000000002") Widgets2 -> UUID.fromString("00000000-0000-0000-0000-000000000002")
@@ -31,15 +31,5 @@ enum class WidgetScreenTarget {
* The default widget screen target * The default widget screen target
*/ */
val Default = Widgets1 val Default = Widgets1
/**
* Get list of available widget screen targets based on configured count.
* @param count Number of widget screens to make available (1-4)
* @return List of WidgetScreenTarget enum values
*/
fun getAvailableTargets(count: Int): List<WidgetScreenTarget> {
val all = listOf(Widgets1, Widgets2, Widgets3, Widgets4)
return all.take(count.coerceIn(1, 4))
}
} }
} }

View File

@@ -379,44 +379,4 @@ class UiSettings internal constructor(
it.copy(widgetsEditButton = editButton) it.copy(widgetsEditButton = editButton)
} }
} }
val widgetScreenCount
get() = launcherDataStore.data.map {
it.widgetScreenCount.coerceIn(1, 4)
}.distinctUntilChanged()
fun setWidgetScreenCount(count: Int) {
val validCount = count.coerceIn(1, 4)
launcherDataStore.update { data ->
var updatedData = data.copy(widgetScreenCount = validCount)
// Auto-reset gestures that point to invalid widget screens
val resetGesture: (GestureAction) -> GestureAction = { action ->
if (action is GestureAction.Widgets) {
val targetIndex = when (action.target) {
WidgetScreenTarget.Widgets1 -> 1
WidgetScreenTarget.Widgets2 -> 2
WidgetScreenTarget.Widgets3 -> 3
WidgetScreenTarget.Widgets4 -> 4
else -> 0
}
if (targetIndex > validCount) GestureAction.NoAction else action
} else {
action
}
}
updatedData = updatedData.copy(
gesturesSwipeUp = resetGesture(updatedData.gesturesSwipeUp),
gesturesSwipeDown = resetGesture(updatedData.gesturesSwipeDown),
gesturesSwipeLeft = resetGesture(updatedData.gesturesSwipeLeft),
gesturesSwipeRight = resetGesture(updatedData.gesturesSwipeRight),
gesturesDoubleTap = resetGesture(updatedData.gesturesDoubleTap),
gesturesLongPress = resetGesture(updatedData.gesturesLongPress),
gesturesHomeButton = resetGesture(updatedData.gesturesHomeButton),
)
updatedData
}
}
} }

View File

@@ -133,7 +133,7 @@ abstract class AppDatabase : RoomDatabase() {
) )
) )
val defaultParentId = WidgetScreenTarget.Default.scopeId val defaultParentId = WidgetScreenTarget.Default.id
db.execSQL( db.execSQL(
"INSERT INTO Widget (`type`, `position`, `id`, `parentId`) VALUES " + "INSERT INTO Widget (`type`, `position`, `id`, `parentId`) VALUES " +
"('weather', 0, ?, ?)," + "('weather', 0, ?, ?)," +

View File

@@ -14,7 +14,7 @@ class Migration_30_31 : Migration(30, 31) {
SET parentId = ? SET parentId = ?
WHERE parentId IS NULL WHERE parentId IS NULL
""".trimIndent(), """.trimIndent(),
arrayOf(WidgetScreenTarget.Default.scopeId.toBytes()), arrayOf(WidgetScreenTarget.Default.id.toBytes()),
) )
} }
} }