Multiple widget panels (#1805)
* Initial approach * Add a new setting so users can choose the amount of widgets screens * Extract indexed string * Remove unnecessary Migration6 * Make HomeComponent use the Default widget id * Add defaultParentId to initial widgets for fresh launch * Fix migration path
This commit is contained in:
@@ -271,7 +271,6 @@ abstract class SharedLauncherActivity(
|
||||
reverse = reverseSearchResults,
|
||||
openKeyboard = searchBarAutofocus,
|
||||
)
|
||||
val widgetComponent by lazy { WidgetsComponent }
|
||||
|
||||
fun getScaffoldGesture(
|
||||
action: GestureAction?,
|
||||
@@ -294,7 +293,7 @@ abstract class SharedLauncherActivity(
|
||||
null
|
||||
} else {
|
||||
ScaffoldGesture(
|
||||
component = widgetComponent,
|
||||
component = WidgetsComponent.forTarget(action.target),
|
||||
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
||||
)
|
||||
}
|
||||
@@ -491,4 +490,4 @@ abstract class SharedLauncherActivity(
|
||||
Launcher,
|
||||
Assistant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import de.mm20.launcher2.preferences.WidgetScreenTarget
|
||||
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
@@ -124,6 +125,7 @@ internal object ClockAndWidgetsHomeComponent : ScaffoldComponent() {
|
||||
scope.launch { state.lock(hideSearchBar = true) }
|
||||
editMode = it
|
||||
},
|
||||
parentId = WidgetScreenTarget.Default.scopeId.toString(),
|
||||
)
|
||||
}
|
||||
if (editMode) {
|
||||
@@ -164,4 +166,4 @@ internal object ClockAndWidgetsHomeComponent : ScaffoldComponent() {
|
||||
super.onDismiss(state)
|
||||
scrollState.scrollTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,32 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import de.mm20.launcher2.preferences.WidgetScreenTarget
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal object WidgetsComponent : ScaffoldComponent() {
|
||||
internal class WidgetsComponent(
|
||||
private val target: WidgetScreenTarget
|
||||
) : ScaffoldComponent() {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Cache for widget component instances.
|
||||
* Components are created lazily only when needed.
|
||||
*/
|
||||
private val componentCache = mutableMapOf<WidgetScreenTarget, WidgetsComponent>()
|
||||
|
||||
/**
|
||||
* Get or create a WidgetsComponent for the given target.
|
||||
* This ensures we reuse the same instance for each target.
|
||||
*/
|
||||
fun forTarget(target: WidgetScreenTarget): WidgetsComponent {
|
||||
return componentCache.getOrPut(target) {
|
||||
WidgetsComponent(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val scrollState = ScrollState(0)
|
||||
|
||||
@@ -98,6 +119,7 @@ internal object WidgetsComponent : ScaffoldComponent() {
|
||||
scope.launch { state.lock(hideSearchBar = true) }
|
||||
editMode = it
|
||||
},
|
||||
parentId = target.scopeId.toString(),
|
||||
)
|
||||
}
|
||||
if (editMode) {
|
||||
@@ -135,4 +157,4 @@ internal object WidgetsComponent : ScaffoldComponent() {
|
||||
super.onDismiss(state)
|
||||
scrollState.scrollTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -41,24 +42,29 @@ import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.widgets.AppWidget
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun WidgetColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
editMode: Boolean = false,
|
||||
onEditModeChange: (Boolean) -> Unit,
|
||||
parentId: String,
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val viewModel: WidgetsVM = viewModel()
|
||||
val viewModel: WidgetsVM = viewModel(
|
||||
key = "widgets-column-$parentId",
|
||||
factory = WidgetsVM.Factory(parentId),
|
||||
)
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
var addNewWidget by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Column {
|
||||
@@ -70,7 +76,8 @@ fun WidgetColumn(
|
||||
for ((i, widget) in widgetsWithIndex) {
|
||||
key(widget.id) {
|
||||
var dragOffsetAfterSwap = remember<Float?> { null }
|
||||
val offsetY = remember(widgets) { mutableStateOf(dragOffsetAfterSwap ?: 0f) }
|
||||
val offsetY =
|
||||
remember(widgets) { mutableFloatStateOf(dragOffsetAfterSwap ?: 0f) }
|
||||
|
||||
LaunchedEffect(widgets) {
|
||||
dragOffsetAfterSwap = null
|
||||
@@ -138,7 +145,7 @@ fun WidgetColumn(
|
||||
scope.launch {
|
||||
offsetY.animateTo(0f)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -148,7 +155,7 @@ fun WidgetColumn(
|
||||
if (editMode || editButton == true) {
|
||||
val title = stringResource(
|
||||
if (editMode) R.string.widget_add_widget
|
||||
else R.string.menu_edit_widgets
|
||||
else R.string.menu_edit_widgets,
|
||||
)
|
||||
|
||||
Button(
|
||||
@@ -162,15 +169,16 @@ fun WidgetColumn(
|
||||
} else {
|
||||
addNewWidget = true
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = ButtonDefaults.IconSpacing)
|
||||
.size(ButtonDefaults.IconSize),
|
||||
painter = painterResource(
|
||||
if (editMode) R.drawable.add_20px else R.drawable.edit_20px
|
||||
), contentDescription = null
|
||||
if (editMode) R.drawable.add_20px else R.drawable.edit_20px,
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(title)
|
||||
}
|
||||
@@ -184,6 +192,6 @@ fun WidgetColumn(
|
||||
onWidgetSelected = {
|
||||
viewModel.addWidget(it)
|
||||
addNewWidget = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import de.mm20.launcher2.preferences.ui.UiSettings
|
||||
import de.mm20.launcher2.widgets.Widget
|
||||
import de.mm20.launcher2.widgets.WidgetRepository
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.util.UUID
|
||||
|
||||
class WidgetsVM : ViewModel(), KoinComponent {
|
||||
class WidgetsVM(
|
||||
private val parentId: UUID?,
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val widgetRepository: WidgetRepository by inject()
|
||||
|
||||
private val uiSettings: UiSettings by inject()
|
||||
@@ -19,7 +24,7 @@ class WidgetsVM : ViewModel(), KoinComponent {
|
||||
val editButton = uiSettings.widgetEditButton
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
val widgets = widgetRepository.get()
|
||||
val widgets = widgetRepository.get(parent = parentId)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
fun addWidget(widget: Widget, index: Int? = null) {
|
||||
@@ -29,7 +34,7 @@ class WidgetsVM : ViewModel(), KoinComponent {
|
||||
} else {
|
||||
widgets.add(index.coerceAtMost(widgets.size), widget)
|
||||
}
|
||||
widgetRepository.set(widgets)
|
||||
widgetRepository.set(widgets, parentId)
|
||||
}
|
||||
|
||||
fun removeWidget(widget: Widget) {
|
||||
@@ -44,13 +49,27 @@ class WidgetsVM : ViewModel(), KoinComponent {
|
||||
val widgets = widgets.value.toMutableList()
|
||||
val widget = widgets.removeAt(index)
|
||||
widgets.add(index - 1, widget)
|
||||
widgetRepository.set(widgets)
|
||||
widgetRepository.set(widgets, parentId)
|
||||
}
|
||||
|
||||
fun moveDown(index: Int) {
|
||||
val widgets = widgets.value.toMutableList()
|
||||
val widget = widgets.removeAt(index)
|
||||
widgets.add(index + 1, widget)
|
||||
widgetRepository.set(widgets)
|
||||
widgetRepository.set(widgets, parentId)
|
||||
}
|
||||
}
|
||||
|
||||
companion object : KoinComponent {
|
||||
fun Factory(parentId: String) = viewModelFactory {
|
||||
initializer {
|
||||
val id = try {
|
||||
UUID.fromString(parentId)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e("WidgetsVM", "Invalid parentId: $parentId", e)
|
||||
null
|
||||
}
|
||||
WidgetsVM(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.navigation3.runtime.NavKey
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
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
|
||||
@@ -48,6 +49,7 @@ fun GestureSettingsScreen() {
|
||||
|
||||
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)
|
||||
@@ -57,7 +59,20 @@ fun GestureSettingsScreen() {
|
||||
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents)
|
||||
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
|
||||
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search)
|
||||
if (allowWidgetGesture == true) add(stringResource(R.string.gesture_action_widgets) to GestureAction.Widgets)
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -89,7 +104,7 @@ fun GestureSettingsScreen() {
|
||||
options = options,
|
||||
app = swipeDownApp,
|
||||
appIcon = swipeDownAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeDownApp(it) }
|
||||
onAppChanged = { viewModel.setSwipeDownApp(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,7 +126,7 @@ fun GestureSettingsScreen() {
|
||||
options = options,
|
||||
app = swipeLeftApp,
|
||||
appIcon = swipeLeftAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeLeftApp(it) }
|
||||
onAppChanged = { viewModel.setSwipeLeftApp(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,7 +148,7 @@ fun GestureSettingsScreen() {
|
||||
options = optionsWithFeed,
|
||||
app = swipeRightApp,
|
||||
appIcon = swipeRightAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeRightApp(it) }
|
||||
onAppChanged = { viewModel.setSwipeRightApp(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,7 +170,7 @@ fun GestureSettingsScreen() {
|
||||
options = options,
|
||||
app = swipeUpApp,
|
||||
appIcon = swipeUpAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeUpApp(it) }
|
||||
onAppChanged = { viewModel.setSwipeUpApp(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,7 +192,7 @@ fun GestureSettingsScreen() {
|
||||
options = options,
|
||||
app = doubleTapApp,
|
||||
appIcon = doubleTapAppIcon,
|
||||
onAppChanged = { viewModel.setDoubleTapApp(it) }
|
||||
onAppChanged = { viewModel.setDoubleTapApp(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,7 +214,7 @@ fun GestureSettingsScreen() {
|
||||
options = options,
|
||||
app = longPressApp,
|
||||
appIcon = longPressAppIcon,
|
||||
onAppChanged = { viewModel.setLongPressApp(it) }
|
||||
onAppChanged = { viewModel.setLongPressApp(it) },
|
||||
)
|
||||
}
|
||||
val homeButton by viewModel.homeButton.collectAsStateWithLifecycle(null)
|
||||
@@ -220,7 +235,7 @@ fun GestureSettingsScreen() {
|
||||
options = options,
|
||||
app = homeButtonApp,
|
||||
appIcon = homeButtonAppIcon,
|
||||
onAppChanged = { viewModel.setHomeButtonApp(it) }
|
||||
onAppChanged = { viewModel.setHomeButtonApp(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -232,7 +247,8 @@ fun requiresAccessibilityService(action: GestureAction?): Boolean {
|
||||
return when (action) {
|
||||
is GestureAction.ScreenLock,
|
||||
is GestureAction.Recents,
|
||||
is GestureAction.PowerMenu -> true
|
||||
is GestureAction.PowerMenu,
|
||||
-> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
@@ -251,19 +267,28 @@ fun GesturePreference(
|
||||
) {
|
||||
var showAppPicker by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
verticalAlignment = (Alignment.CenterVertically)
|
||||
verticalAlignment = (Alignment.CenterVertically),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
ListPreference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
items = options,
|
||||
value = value,
|
||||
summary = options.find { value?.javaClass == it.second.javaClass }?.first
|
||||
?: stringResource(R.string.gesture_action_none),
|
||||
onValueChanged = { if (it != null) onValueChanged(it) }
|
||||
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) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -273,12 +298,13 @@ fun GesturePreference(
|
||||
.height(36.dp)
|
||||
.width(1.dp)
|
||||
.alpha(0.38f)
|
||||
.background(LocalContentColor.current)
|
||||
.background(LocalContentColor.current),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable { showAppPicker = true }
|
||||
.padding(12.dp)) {
|
||||
.padding(12.dp),
|
||||
) {
|
||||
ShapedLauncherIcon(size = 32.dp, icon = { appIcon })
|
||||
}
|
||||
}
|
||||
@@ -295,8 +321,8 @@ fun GesturePreference(
|
||||
onValueChanged = {
|
||||
showAppPicker = false
|
||||
onAppChanged(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
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
|
||||
@@ -187,4 +190,4 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
if (searchable == null) return emptyFlow()
|
||||
return iconService.getIcon(searchable, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,28 @@ 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),
|
||||
@@ -442,4 +464,4 @@ fun SearchBarStylePreference(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,13 @@ 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 {
|
||||
@@ -177,4 +184,4 @@ class HomescreenSettingsScreenVM(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +686,8 @@
|
||||
<string name="preference_widgets_edit_button_summary">Show a button to add, remove and rearrange widgets</string>
|
||||
<string name="preference_widgets_on_home_screen">Widgets on home screen</string>
|
||||
<string name="preference_widgets_on_home_screen_summary">Show widgets on the home screen instead of a separate page</string>
|
||||
<string name="preference_widget_screen_count">Widget screen count</string>
|
||||
<string name="preference_widget_screen_count_info">Note: Reducing the count will automatically reset gestures assigned to disabled screens.</string>
|
||||
<string name="preference_screen_homescreen">Home screen</string>
|
||||
<string name="preference_screen_homescreen_summary">Clock, search bar, wallpaper, system bars</string>
|
||||
<string name="preference_screen_icons">Grid and icons</string>
|
||||
@@ -809,6 +811,7 @@
|
||||
<string name="gesture_action_none">Do nothing</string>
|
||||
<string name="gesture_action_open_search">Open search</string>
|
||||
<string name="gesture_action_widgets">Widgets</string>
|
||||
<string name="gesture_action_widgets_indexed">Widgets %1$d</string>
|
||||
<string name="gesture_action_launch_app">Launch app</string>
|
||||
<string name="gesture_action_notifications">Open notification drawer</string>
|
||||
<string name="gesture_action_lock_screen">Turn off screen</string>
|
||||
|
||||
@@ -29,4 +29,4 @@ internal class LauncherDataStore(
|
||||
fun update(block: (LauncherSettingsData) -> LauncherSettingsData) {
|
||||
updateData(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.UUID
|
||||
@Serializable
|
||||
@ConsistentCopyVisibility
|
||||
data class LauncherSettingsData internal constructor(
|
||||
val schemaVersion: Int = 5,
|
||||
val schemaVersion: Int = 6,
|
||||
|
||||
val uiColorScheme: ColorScheme = ColorScheme.System,
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
@@ -143,11 +143,12 @@ data class LauncherSettingsData internal constructor(
|
||||
val surfacesShape: SurfaceShape = SurfaceShape.Rounded,
|
||||
|
||||
val widgetsEditButton: Boolean = true,
|
||||
val widgetScreenCount: Int = 1,
|
||||
|
||||
val gesturesSwipeDown: GestureAction = GestureAction.Search,
|
||||
val gesturesSwipeLeft: GestureAction = GestureAction.NoAction,
|
||||
val gesturesSwipeRight: GestureAction = GestureAction.NoAction,
|
||||
val gesturesSwipeUp: GestureAction = GestureAction.Widgets,
|
||||
val gesturesSwipeUp: GestureAction = GestureAction.Widgets(),
|
||||
val gesturesDoubleTap: GestureAction = GestureAction.ScreenLock,
|
||||
val gesturesLongPress: GestureAction = GestureAction.NoAction,
|
||||
val gesturesHomeButton: GestureAction = GestureAction.NoAction,
|
||||
@@ -376,7 +377,7 @@ sealed interface GestureAction {
|
||||
|
||||
@Serializable
|
||||
@SerialName("widgets")
|
||||
data object Widgets : GestureAction
|
||||
data class Widgets(val target: WidgetScreenTarget = WidgetScreenTarget.Default) : GestureAction
|
||||
|
||||
@Serializable
|
||||
@SerialName("power_menu")
|
||||
@@ -445,4 +446,4 @@ enum class MeasurementSystem {
|
||||
@SerialName("metric") Metric,
|
||||
@SerialName("uk") UnitedKingdom,
|
||||
@SerialName("us") UnitedStates,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.mm20.launcher2.preferences
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Represents different widget screen targets.
|
||||
* Each target corresponds to a separate widget area with its own scope.
|
||||
*/
|
||||
@Serializable
|
||||
enum class WidgetScreenTarget {
|
||||
Widgets1,
|
||||
Widgets2,
|
||||
Widgets3,
|
||||
Widgets4;
|
||||
|
||||
/**
|
||||
* Returns the UUID used as parent identifier for widget repository operations.
|
||||
* These are deterministic UUIDs generated using UUID v5 (name-based).
|
||||
*/
|
||||
val scopeId: UUID
|
||||
get() = when (this) {
|
||||
Widgets1 -> UUID.fromString("00000000-0000-0000-0000-000000000001")
|
||||
Widgets2 -> UUID.fromString("00000000-0000-0000-0000-000000000002")
|
||||
Widgets3 -> UUID.fromString("00000000-0000-0000-0000-000000000003")
|
||||
Widgets4 -> UUID.fromString("00000000-0000-0000-0000-000000000004")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 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<WidgetScreenTarget> {
|
||||
val all = listOf(Widgets1, Widgets2, Widgets3, Widgets4)
|
||||
return all.take(count.coerceIn(1, 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class Migration5 : DataMigration<LauncherSettingsData> {
|
||||
gesturesSwipeDown = if (currentData.uiBaseLayout == BaseLayout.PullDown) GestureAction.Search else currentData.gesturesSwipeDown,
|
||||
gesturesSwipeLeft = if (currentData.uiBaseLayout == BaseLayout.Pager) GestureAction.Search else currentData.gesturesSwipeLeft,
|
||||
gesturesSwipeRight = if (currentData.uiBaseLayout == BaseLayout.PagerReversed) GestureAction.Search else currentData.gesturesSwipeRight,
|
||||
gesturesSwipeUp = GestureAction.Widgets,
|
||||
gesturesSwipeUp = GestureAction.Widgets(),
|
||||
homeScreenWidgets = !currentData.clockWidgetFillHeight,
|
||||
)
|
||||
}
|
||||
@@ -23,4 +23,4 @@ class Migration5 : DataMigration<LauncherSettingsData> {
|
||||
override suspend fun shouldMigrate(currentData: LauncherSettingsData): Boolean {
|
||||
return currentData.schemaVersion < 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package de.mm20.launcher2.preferences.ui
|
||||
|
||||
import de.mm20.launcher2.preferences.ColorScheme
|
||||
import de.mm20.launcher2.preferences.GestureAction
|
||||
import de.mm20.launcher2.preferences.IconShape
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.ScreenOrientation
|
||||
import de.mm20.launcher2.preferences.SearchBarColors
|
||||
import de.mm20.launcher2.preferences.SearchBarStyle
|
||||
import de.mm20.launcher2.preferences.SystemBarColors
|
||||
import de.mm20.launcher2.preferences.WidgetScreenTarget
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.UUID
|
||||
@@ -377,4 +379,44 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +39,17 @@ import de.mm20.launcher2.database.migrations.Migration_22_23
|
||||
import de.mm20.launcher2.database.migrations.Migration_23_24
|
||||
import de.mm20.launcher2.database.migrations.Migration_24_25
|
||||
import de.mm20.launcher2.database.migrations.Migration_25_26
|
||||
import de.mm20.launcher2.database.migrations.Migration_26_27
|
||||
import de.mm20.launcher2.database.migrations.Migration_27_28
|
||||
import de.mm20.launcher2.database.migrations.Migration_28_29
|
||||
import de.mm20.launcher2.database.migrations.Migration_29_30
|
||||
import de.mm20.launcher2.database.migrations.Migration_30_31
|
||||
import de.mm20.launcher2.database.migrations.Migration_6_7
|
||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||
import de.mm20.launcher2.database.migrations.Migration_9_10
|
||||
import de.mm20.launcher2.ktx.toBytes
|
||||
import de.mm20.launcher2.preferences.WidgetScreenTarget
|
||||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
@@ -64,7 +67,7 @@ import java.util.UUID
|
||||
ShapesEntity::class,
|
||||
TransparenciesEntity::class,
|
||||
TypographyEntity::class,
|
||||
], version = 30, exportSchema = true
|
||||
], version = 31, exportSchema = true
|
||||
)
|
||||
@TypeConverters(ComponentNameConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -129,15 +132,19 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
)
|
||||
)
|
||||
|
||||
val defaultParentId = WidgetScreenTarget.Default.scopeId
|
||||
db.execSQL(
|
||||
"INSERT INTO Widget (`type`, `position`, `id`) VALUES " +
|
||||
"('weather', 0, ?)," +
|
||||
"('music', 1, ?)," +
|
||||
"('calendar', 2, ?);",
|
||||
"INSERT INTO Widget (`type`, `position`, `id`, `parentId`) VALUES " +
|
||||
"('weather', 0, ?, ?)," +
|
||||
"('music', 1, ?, ?)," +
|
||||
"('calendar', 2, ?, ?);",
|
||||
arrayOf(
|
||||
UUID.randomUUID().toBytes(),
|
||||
defaultParentId.toBytes(),
|
||||
UUID.randomUUID().toBytes(),
|
||||
UUID.randomUUID().toBytes()
|
||||
defaultParentId.toBytes(),
|
||||
UUID.randomUUID().toBytes(),
|
||||
defaultParentId.toBytes()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -163,13 +170,14 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration_23_24(),
|
||||
Migration_24_25(),
|
||||
Migration_25_26(),
|
||||
Migration_26_27(),
|
||||
Migration_27_28(),
|
||||
Migration_28_29(),
|
||||
Migration_29_30(),
|
||||
Migration_30_31(),
|
||||
).build()
|
||||
if (_instance == null) _instance = instance
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration_26_27: Migration(26, 27) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import de.mm20.launcher2.ktx.toBytes
|
||||
import de.mm20.launcher2.preferences.WidgetScreenTarget
|
||||
|
||||
class Migration_30_31 : Migration(30, 31) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE Widget
|
||||
SET parentId = ?
|
||||
WHERE parentId IS NULL
|
||||
""".trimIndent(),
|
||||
arrayOf(WidgetScreenTarget.Default.scopeId.toBytes()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user