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 1f1b4cad6..6603aa194 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 @@ -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 } -} \ No newline at end of file +} 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 4464cdeb4..e779cec68 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 @@ -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) } -} \ No newline at end of file +} 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 a8e890cb5..bade9eea2 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 @@ -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() + + /** + * 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) } -} \ No newline at end of file +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index 100fb00be..2e78a97ac 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -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 { 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 - } + }, ) -} \ No newline at end of file +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt index 79d51e49d..d22be0d5c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt @@ -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) } -} \ No newline at end of file + + 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) + } + } + } +} 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 69b90494e..79b9d9c18 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 @@ -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) - } + }, ) } -} \ No newline at end of file +} 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 1f2fc989a..a8bd81db7 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 @@ -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) } -} \ 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 ec318196e..f95955beb 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,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( } } } -} \ No newline at end of file +} 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 e1cd58c2c..5c5f624fe 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,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( } } } -} \ No newline at end of file +} diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 679678757..8c5c75c42 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -686,6 +686,8 @@ Show a button to add, remove and rearrange widgets Widgets on home screen Show widgets on the home screen instead of a separate page + Widget screen count + Note: Reducing the count will automatically reset gestures assigned to disabled screens. Home screen Clock, search bar, wallpaper, system bars Grid and icons @@ -809,6 +811,7 @@ Do nothing Open search Widgets + Widgets %1$d Launch app Open notification drawer Turn off screen diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherDataStore.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherDataStore.kt index 04906b4cb..9168ba0d4 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherDataStore.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherDataStore.kt @@ -29,4 +29,4 @@ internal class LauncherDataStore( fun update(block: (LauncherSettingsData) -> LauncherSettingsData) { updateData(block) } -} \ No newline at end of file +} diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 13e290283..8c9d294e1 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -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, -} \ No newline at end of file +} 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 new file mode 100644 index 000000000..ea22f19fd --- /dev/null +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/WidgetScreenTarget.kt @@ -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 { + 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/migrations/Migration5.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration5.kt index 1e785aa20..58a85d0a1 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration5.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration5.kt @@ -15,7 +15,7 @@ class Migration5 : DataMigration { 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 { override suspend fun shouldMigrate(currentData: LauncherSettingsData): Boolean { return currentData.schemaVersion < 5 } -} \ No newline at end of file +} 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 076b56062..0cd1c213f 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 @@ -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 + } + } } 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 b5238d918..a75212043 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 @@ -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 } } } - diff --git a/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt new file mode 100644 index 000000000..321d79112 --- /dev/null +++ b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt @@ -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 + } +} 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 new file mode 100644 index 000000000..e71a719c1 --- /dev/null +++ b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_30_31.kt @@ -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()), + ) + } +}