Redesign gesture settings
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.DockedSearchBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -34,128 +38,153 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
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.ktx.animateShapeAsState
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
|
||||
@Composable
|
||||
fun SearchablePicker(
|
||||
expanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(),
|
||||
value: SavableSearchable?,
|
||||
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(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp + WindowInsets.navigationBars.asPaddingValues()
|
||||
.calculateBottomPadding(),
|
||||
top = 16.dp,
|
||||
)
|
||||
) {
|
||||
stickyHeader {
|
||||
DockedSearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.drawBehind {
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(
|
||||
0.5f to colorSurface,
|
||||
0.5f to colorSurface.copy(alpha = 0f),
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(1.dp)
|
||||
) {
|
||||
stickyHeader {
|
||||
DockedSearchBar(
|
||||
modifier = Modifier
|
||||
.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))
|
||||
},
|
||||
)
|
||||
}
|
||||
) {
|
||||
}
|
||||
}
|
||||
items(viewModel.items) {
|
||||
val iconSize = 32.dp.toPixels()
|
||||
val icon by remember(it.key) {
|
||||
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) {
|
||||
.padding(bottom = 16.dp),
|
||||
expanded = false,
|
||||
onExpandedChange = {},
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.check_circle_24px_filled),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
painterResource(R.drawable.search_24px),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
@@ -24,6 +26,7 @@ class SearchablePickerVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val searchService: SearchService by inject()
|
||||
private val iconService: IconService by inject()
|
||||
private val badgeService: BadgeService by inject()
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
@@ -54,4 +57,10 @@ class SearchablePickerVM : ViewModel(), KoinComponent {
|
||||
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {
|
||||
return iconService.getIcon(searchable, size)
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun getBadge(searchable: SavableSearchable): Flow<Badge?> {
|
||||
return badgeService.getBadge(searchable)
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ 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.WidgetScreenTarget
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.base.BaseActivity
|
||||
import de.mm20.launcher2.ui.base.ProvideCompositionLocals
|
||||
@@ -289,7 +290,7 @@ abstract class SharedLauncherActivity(
|
||||
)
|
||||
|
||||
is GestureAction.Widgets ->
|
||||
if (widgetsOnHomeScreen == true) {
|
||||
if (widgetsOnHomeScreen == true && action.target == WidgetScreenTarget.Default) {
|
||||
null
|
||||
} else {
|
||||
ScaffoldGesture(
|
||||
|
||||
@@ -125,7 +125,7 @@ internal object ClockAndWidgetsHomeComponent : ScaffoldComponent() {
|
||||
scope.launch { state.lock(hideSearchBar = true) }
|
||||
editMode = it
|
||||
},
|
||||
parentId = WidgetScreenTarget.Default.scopeId.toString(),
|
||||
parentId = WidgetScreenTarget.Default.id.toString(),
|
||||
)
|
||||
}
|
||||
if (editMode) {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
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.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
@@ -38,8 +35,6 @@ 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
|
||||
@@ -119,7 +114,7 @@ internal class WidgetsComponent(
|
||||
scope.launch { state.lock(hideSearchBar = true) }
|
||||
editMode = it
|
||||
},
|
||||
parentId = target.scopeId.toString(),
|
||||
parentId = target.id.toString(),
|
||||
)
|
||||
}
|
||||
if (editMode) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -20,6 +23,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -49,51 +53,33 @@ fun GestureSettingsScreen() {
|
||||
val viewModel: GestureSettingsScreenVM = viewModel()
|
||||
|
||||
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
|
||||
val allowWidgetGesture by viewModel.allowWidgetGesture.collectAsStateWithLifecycle(null)
|
||||
val widgetScreenCount by viewModel.widgetScreenCount.collectAsStateWithLifecycle(1)
|
||||
|
||||
val options = buildList {
|
||||
add(stringResource(R.string.gesture_action_none) to GestureAction.NoAction)
|
||||
add(stringResource(R.string.gesture_action_notifications) to GestureAction.Notifications)
|
||||
add(stringResource(R.string.gesture_action_quick_settings) to GestureAction.QuickSettings)
|
||||
if (isAtLeastApiLevel(28)) add(stringResource(R.string.gesture_action_lock_screen) to GestureAction.ScreenLock)
|
||||
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents)
|
||||
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
|
||||
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search)
|
||||
if (allowWidgetGesture == true) {
|
||||
// Dynamically add widget screen targets based on user configuration
|
||||
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 options = buildSet {
|
||||
add(GestureAction.NoAction::class)
|
||||
add(GestureAction.Notifications::class)
|
||||
add(GestureAction.QuickSettings::class)
|
||||
if (isAtLeastApiLevel(28)) add(GestureAction.ScreenLock::class)
|
||||
add(GestureAction.Recents::class)
|
||||
add(GestureAction.PowerMenu::class)
|
||||
add(GestureAction.Search::class)
|
||||
add(GestureAction.Widgets::class)
|
||||
add(GestureAction.Launch::class)
|
||||
}
|
||||
|
||||
val shortcutOptions by viewModel.shortcutOptions.collectAsStateWithLifecycle(emptyList())
|
||||
val widgetOptions by viewModel.widgetOptions.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val optionsWithFeed =
|
||||
if (FeatureFlags.feed) {
|
||||
options + (stringResource(R.string.gesture_action_feed) to GestureAction.Feed)
|
||||
options + GestureAction.Feed::class
|
||||
} else options
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
PreferenceScreen(title = stringResource(R.string.preference_screen_gestures)) {
|
||||
item {
|
||||
val appIconSize = 32.dp.toPixels()
|
||||
PreferenceCategory {
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(swipeDown),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -103,19 +89,14 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_swipe_down),
|
||||
icon = R.drawable.swipe_down_alt_24px,
|
||||
value = swipeDown,
|
||||
onValueChanged = { viewModel.setSwipeDown(it) },
|
||||
onValueChanged = viewModel::setSwipeDown,
|
||||
options = options,
|
||||
app = swipeDownApp,
|
||||
appIcon = swipeDownAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeDownApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(swipeLeft),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -125,19 +106,14 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_swipe_left),
|
||||
icon = R.drawable.swipe_left_alt_24px,
|
||||
value = swipeLeft,
|
||||
onValueChanged = { viewModel.setSwipeLeft(it) },
|
||||
onValueChanged = viewModel::setSwipeLeft,
|
||||
options = options,
|
||||
app = swipeLeftApp,
|
||||
appIcon = swipeLeftAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeLeftApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(swipeRight),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -147,19 +123,14 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_swipe_right),
|
||||
icon = R.drawable.swipe_right_alt_24px,
|
||||
value = swipeRight,
|
||||
onValueChanged = { viewModel.setSwipeRight(it) },
|
||||
onValueChanged = viewModel::setSwipeRight,
|
||||
options = optionsWithFeed,
|
||||
app = swipeRightApp,
|
||||
appIcon = swipeRightAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeRightApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(swipeUp),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -169,19 +140,14 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_swipe_up),
|
||||
icon = R.drawable.swipe_up_alt_24px,
|
||||
value = swipeUp,
|
||||
onValueChanged = { viewModel.setSwipeUp(it) },
|
||||
onValueChanged = viewModel::setSwipeUp,
|
||||
options = options,
|
||||
app = swipeUpApp,
|
||||
appIcon = swipeUpAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeUpApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(doubleTap),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -191,19 +157,14 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_double_tap),
|
||||
icon = R.drawable.adjust_24px,
|
||||
value = doubleTap,
|
||||
onValueChanged = { viewModel.setDoubleTap(it) },
|
||||
onValueChanged = viewModel::setDoubleTap,
|
||||
options = options,
|
||||
app = doubleTapApp,
|
||||
appIcon = doubleTapAppIcon,
|
||||
onAppChanged = { viewModel.setDoubleTapApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(longPress),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -213,18 +174,13 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_long_press),
|
||||
icon = R.drawable.circle_24px,
|
||||
value = longPress,
|
||||
onValueChanged = { viewModel.setLongPress(it) },
|
||||
onValueChanged = viewModel::setLongPress,
|
||||
options = options,
|
||||
app = longPressApp,
|
||||
appIcon = longPressAppIcon,
|
||||
onAppChanged = { viewModel.setLongPressApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
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(
|
||||
locked = hasPermission == false && requiresAccessibilityService(homeButton),
|
||||
description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
|
||||
@@ -234,11 +190,10 @@ fun GestureSettingsScreen() {
|
||||
title = stringResource(R.string.preference_gesture_home_button),
|
||||
icon = R.drawable.home_24px,
|
||||
value = homeButton,
|
||||
onValueChanged = { viewModel.setHomeButton(it) },
|
||||
onValueChanged = viewModel::setHomeButton,
|
||||
options = options,
|
||||
app = homeButtonApp,
|
||||
appIcon = homeButtonAppIcon,
|
||||
onAppChanged = { viewModel.setHomeButtonApp(it) },
|
||||
shortcutOptions = shortcutOptions,
|
||||
widgetOptions = widgetOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -257,75 +212,88 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GesturePreference(
|
||||
title: String,
|
||||
@DrawableRes icon: Int,
|
||||
value: GestureAction?,
|
||||
onValueChanged: (GestureAction) -> Unit,
|
||||
options: List<Pair<String, GestureAction>>,
|
||||
app: SavableSearchable?,
|
||||
appIcon: LauncherIcon?,
|
||||
onAppChanged: (SavableSearchable?) -> Unit,
|
||||
) {
|
||||
var showAppPicker by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
verticalAlignment = (Alignment.CenterVertically),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
ListPreference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
items = options,
|
||||
value = value,
|
||||
summary = options.find { option ->
|
||||
when {
|
||||
value is GestureAction.Widgets && option.second is GestureAction.Widgets -> {
|
||||
val valueTarget = value.target
|
||||
val optionTarget = (option.second as GestureAction.Widgets).target
|
||||
valueTarget == optionTarget
|
||||
}
|
||||
|
||||
else -> value?.javaClass == option.second.javaClass
|
||||
}
|
||||
}?.first ?: stringResource(R.string.gesture_action_none),
|
||||
onValueChanged = { if (it != null) onValueChanged(it) },
|
||||
)
|
||||
}
|
||||
|
||||
if (value is GestureAction.Launch) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.width(1.dp)
|
||||
.alpha(0.38f)
|
||||
.background(LocalContentColor.current),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable { showAppPicker = true }
|
||||
.padding(12.dp),
|
||||
) {
|
||||
ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
//@Composable
|
||||
//fun GesturePreference(
|
||||
// title: String,
|
||||
// @DrawableRes icon: Int,
|
||||
// value: GestureAction?,
|
||||
// onValueChanged: (GestureAction) -> Unit,
|
||||
// options: List<Pair<String, GestureAction>>,
|
||||
// app: SavableSearchable?,
|
||||
// appIcon: LauncherIcon?,
|
||||
// onAppChanged: (SavableSearchable?) -> Unit,
|
||||
//) {
|
||||
// var showAppPicker by remember { mutableStateOf(false) }
|
||||
// Row(
|
||||
// verticalAlignment = (Alignment.CenterVertically),
|
||||
// ) {
|
||||
// Box(
|
||||
// modifier = Modifier.weight(1f),
|
||||
// ) {
|
||||
// ListPreference(
|
||||
// title = title,
|
||||
// icon = icon,
|
||||
// items = options,
|
||||
// value = value,
|
||||
// summary = options.find { option ->
|
||||
// when {
|
||||
// value is GestureAction.Widgets && option.second is GestureAction.Widgets -> {
|
||||
// val valueTarget = value.target
|
||||
// val optionTarget = (option.second as GestureAction.Widgets).target
|
||||
// valueTarget == optionTarget
|
||||
// }
|
||||
//
|
||||
// else -> value?.javaClass == option.second.javaClass
|
||||
// }
|
||||
// }?.first ?: stringResource(R.string.gesture_action_none),
|
||||
// onValueChanged = { if (it != null) onValueChanged(it) },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// if (value is GestureAction.Launch) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .height(36.dp)
|
||||
// .width(1.dp)
|
||||
// .background(MaterialTheme.colorScheme.outlineVariant),
|
||||
// )
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .clickable { showAppPicker = true }
|
||||
// .padding(12.dp),
|
||||
// ) {
|
||||
// ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
|
||||
// }
|
||||
// }
|
||||
// if (value is GestureAction.Widgets) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .height(36.dp)
|
||||
// .width(1.dp)
|
||||
// .background(MaterialTheme.colorScheme.outlineVariant),
|
||||
// )
|
||||
// IconButton(
|
||||
// modifier = Modifier.padding(4.dp),
|
||||
// onClick = {}
|
||||
// ) {
|
||||
// Icon(painterResource(R.drawable.tune_24px), "")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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)
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -7,39 +7,36 @@ import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.BaseLayout
|
||||
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.UiSettings
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
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.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
internal class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val gestureSettings: GestureSettings by inject()
|
||||
private val uiSettings: UiSettings by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
private val iconService: IconService by inject()
|
||||
private val widgetRepository: WidgetRepository by inject()
|
||||
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
|
||||
.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
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
val swipeLeft = gestureSettings.swipeLeft
|
||||
@@ -55,131 +52,101 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
val homeButton = gestureSettings.homeButton
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
fun setSwipeDown(action: GestureAction) {
|
||||
fun setSwipeDown(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setSwipeDown(action)
|
||||
}
|
||||
|
||||
fun setSwipeLeft(action: GestureAction) {
|
||||
fun setSwipeLeft(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setSwipeLeft(action)
|
||||
}
|
||||
|
||||
fun setSwipeRight(action: GestureAction) {
|
||||
fun setSwipeRight(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setSwipeRight(action)
|
||||
}
|
||||
|
||||
fun setSwipeUp(action: GestureAction) {
|
||||
fun setSwipeUp(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setSwipeUp(action)
|
||||
}
|
||||
|
||||
fun setDoubleTap(action: GestureAction) {
|
||||
fun setDoubleTap(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setDoubleTap(action)
|
||||
}
|
||||
|
||||
fun setLongPress(action: GestureAction) {
|
||||
fun setLongPress(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setLongPress(action)
|
||||
}
|
||||
|
||||
fun setHomeButton(action: GestureAction) {
|
||||
fun setHomeButton(action: GestureAction, searchable: SavableSearchable?) {
|
||||
saveShortcut(searchable)
|
||||
gestureSettings.setHomeButton(action)
|
||||
}
|
||||
|
||||
val swipeLeftApp: Flow<SavableSearchable?> = swipeLeft
|
||||
.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 setSwipeLeftApp(searchable: SavableSearchable?) {
|
||||
searchable?.let { searchableRepository.insert(it) } ?: return
|
||||
setSwipeLeft(GestureAction.Launch(searchable.key))
|
||||
private fun saveShortcut(searchable: SavableSearchable?) {
|
||||
searchable?.let { searchableRepository.insert(it) }
|
||||
}
|
||||
|
||||
val swipeRightApp: Flow<SavableSearchable?> = swipeRight
|
||||
.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 setSwipeRightApp(searchable: SavableSearchable?) {
|
||||
searchable?.let { searchableRepository.insert(it) } ?: return
|
||||
setSwipeRight(GestureAction.Launch(searchable.key))
|
||||
val shortcutOptions: Flow<List<SavableSearchable>> = gestureSettings.flatMapLatest {
|
||||
val keys = listOfNotNull(
|
||||
(it.swipeUp as? GestureAction.Launch)?.key,
|
||||
(it.swipeLeft as? GestureAction.Launch)?.key,
|
||||
(it.swipeRight as? GestureAction.Launch)?.key,
|
||||
(it.swipeDown as? GestureAction.Launch)?.key,
|
||||
(it.doubleTap as? GestureAction.Launch)?.key,
|
||||
(it.homeButton as? GestureAction.Launch)?.key,
|
||||
(it.longPress as? GestureAction.Launch)?.key,
|
||||
)
|
||||
searchableRepository.getByKeys(keys)
|
||||
}
|
||||
|
||||
val swipeDownApp: Flow<SavableSearchable?> = swipeDown
|
||||
.flatMapLatest {
|
||||
if (it !is GestureAction.Launch || it.key == null) flowOf(null)
|
||||
else searchableRepository.getByKeys(listOf(it.key!!)).map {
|
||||
it.firstOrNull()
|
||||
val widgetOptions: Flow<List<WidgetPageOption>> =
|
||||
combine(uiSettings.homeScreenWidgets, gestureSettings) { home, gestures ->
|
||||
val options = mutableListOf<WidgetPageOption>()
|
||||
val usedTargets = setOfNotNull(
|
||||
(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) {
|
||||
@@ -191,3 +158,9 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
return iconService.getIcon(searchable, size)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class WidgetPageOption(
|
||||
val id: WidgetScreenTarget,
|
||||
val widgets: List<Widget>,
|
||||
val isNewPage: Boolean,
|
||||
)
|
||||
@@ -135,28 +135,6 @@ fun HomescreenSettingsScreen() {
|
||||
onValueChanged = {
|
||||
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(
|
||||
title = stringResource(id = R.string.preference_edit_button),
|
||||
summary = stringResource(id = R.string.preference_widgets_edit_button_summary),
|
||||
|
||||
@@ -166,13 +166,6 @@ class HomescreenSettingsScreenVM(
|
||||
uiSettings.setHomeScreenWidgets(widgetsOnHomeScreen)
|
||||
}
|
||||
|
||||
val widgetScreenCount = uiSettings.widgetScreenCount
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||
|
||||
fun setWidgetScreenCount(count: Int) {
|
||||
uiSettings.setWidgetScreenCount(count)
|
||||
}
|
||||
|
||||
companion object : KoinComponent {
|
||||
val Factory = viewModelFactory {
|
||||
initializer {
|
||||
|
||||
Reference in New Issue
Block a user