diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt index f58befb58..26fe9f9ad 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt @@ -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, + ) } } } } } -} \ No newline at end of file +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt index 5594672b4..31d1debf0 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt @@ -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 { return iconService.getIcon(searchable, size) } + + + + fun getBadge(searchable: SavableSearchable): Flow { + return badgeService.getBadge(searchable) + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt index 6603aa194..99b70cb4d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt @@ -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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt index e779cec68..b9d300080 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/ClockAndWidgetsHomeComponent.kt @@ -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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt index bade9eea2..0fd223f3d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/WidgetsComponent.kt @@ -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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GesturePreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GesturePreference.kt new file mode 100644 index 000000000..2e5303019 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GesturePreference.kt @@ -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>, + shortcutOptions: List, + widgetOptions: List, +) { + 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 +): 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) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt index e16560670..6736e39e7 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt @@ -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>, - 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>, +// 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) +// }, +// ) +// } +// +//} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt index a8bd81db7..284720ebc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt @@ -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 = 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 = 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> = 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 = swipeDown - .flatMapLatest { - if (it !is GestureAction.Launch || it.key == null) flowOf(null) - else searchableRepository.getByKeys(listOf(it.key!!)).map { - it.firstOrNull() + val widgetOptions: Flow> = + combine(uiSettings.homeScreenWidgets, gestureSettings) { home, gestures -> + val options = mutableListOf() + 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 = 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 = 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 = 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 = 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, + val isNewPage: Boolean, +) \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt index f95955beb..85f7e27e5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt @@ -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), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreenVM.kt index 5c5f624fe..c5957aa19 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreenVM.kt @@ -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 { diff --git a/core/base/src/main/res/drawable/action_key_24px.xml b/core/base/src/main/res/drawable/action_key_24px.xml new file mode 100644 index 000000000..0f468a5fe --- /dev/null +++ b/core/base/src/main/res/drawable/action_key_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/base/src/main/res/drawable/amp_stories_24px.xml b/core/base/src/main/res/drawable/amp_stories_24px.xml new file mode 100644 index 000000000..57453f194 --- /dev/null +++ b/core/base/src/main/res/drawable/amp_stories_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/base/src/main/res/drawable/widgets_add_24px.xml b/core/base/src/main/res/drawable/widgets_add_24px.xml new file mode 100644 index 000000000..49de6f7d0 --- /dev/null +++ b/core/base/src/main/res/drawable/widgets_add_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index a6a006df8..25481561e 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -812,15 +812,21 @@ Long press Home button/gesture Do nothing - Open search + Launcher pages + System actions + Apps and shortcuts + Search / app drawer Widgets - Widgets %1$d + New widgets page + Empty + Widgets (%1$d) Launch app - Open notification drawer + Select app or shortcut + Notifications Turn off screen - Open quick settings - Show power menu - Open recent apps + Quick settings + Power menu + Recent apps Feed 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: The launcher\'s accessibility service needs to be enabled to perform this action. diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/WidgetScreenTarget.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/WidgetScreenTarget.kt index ea22f19fd..4c752440a 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/WidgetScreenTarget.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/WidgetScreenTarget.kt @@ -18,7 +18,7 @@ enum class WidgetScreenTarget { * Returns the UUID used as parent identifier for widget repository operations. * These are deterministic UUIDs generated using UUID v5 (name-based). */ - val scopeId: UUID + val id: UUID get() = when (this) { Widgets1 -> UUID.fromString("00000000-0000-0000-0000-000000000001") Widgets2 -> UUID.fromString("00000000-0000-0000-0000-000000000002") @@ -31,15 +31,5 @@ enum class WidgetScreenTarget { * The default widget screen target */ 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 { - val all = listOf(Widgets1, Widgets2, Widgets3, Widgets4) - return all.take(count.coerceIn(1, 4)) - } } } diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt index 0cd1c213f..3d2e55518 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt @@ -379,44 +379,4 @@ class UiSettings internal constructor( 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 - } - } } diff --git a/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index 2bf8e49b2..ff147354c 100644 --- a/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -133,7 +133,7 @@ abstract class AppDatabase : RoomDatabase() { ) ) - val defaultParentId = WidgetScreenTarget.Default.scopeId + val defaultParentId = WidgetScreenTarget.Default.id db.execSQL( "INSERT INTO Widget (`type`, `position`, `id`, `parentId`) VALUES " + "('weather', 0, ?, ?)," + diff --git a/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_30_31.kt b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_30_31.kt index e71a719c1..9f3ecaa6c 100644 --- a/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_30_31.kt +++ b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_30_31.kt @@ -14,7 +14,7 @@ class Migration_30_31 : Migration(30, 31) { SET parentId = ? WHERE parentId IS NULL """.trimIndent(), - arrayOf(WidgetScreenTarget.Default.scopeId.toBytes()), + arrayOf(WidgetScreenTarget.Default.id.toBytes()), ) } }