diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml
index 5369684f21..cc7f515900 100644
--- a/lawnchair/res/values/strings.xml
+++ b/lawnchair/res/values/strings.xml
@@ -125,6 +125,8 @@
Caddy
App drawer folders
+ Selected apps
+ Add apps
Drawer folder
Create folder
Edit folder
diff --git a/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt b/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt
index c9d9f11fd2..a12c735986 100644
--- a/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt
+++ b/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt
@@ -60,8 +60,12 @@ class FolderViewModel(
fun updateFolderItems(id: Int, title: String, appInfo: List) {
viewModelScope.launch {
repository.updateFolderWithItems(id, title, appInfo)
+ // Update the local state flow so UI can observe changes without full reload if needed,
+ // though for now we just rely on reloadGrid to refresh the launcher.
+ // We call reloadGrid *after* the DB update is complete.
+ _folderInfo.value = repository.getFolderInfo(id, true)
+ reloadHelper.reloadGrid()
}
- reloadHelper.reloadGrid()
}
fun createFolder(folderInfo: FolderInfo) {
diff --git a/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt b/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt
index 9305db4baf..c82b9b8194 100644
--- a/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt
+++ b/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt
@@ -38,8 +38,8 @@ class FolderService(val context: Context) : SafeCloseable {
suspend fun updateFolderWithItems(folderInfoId: Int, title: String, appInfos: List) = withContext(Dispatchers.IO) {
folderDao.insertFolderWithItems(
FolderInfoEntity(id = folderInfoId, title = title),
- appInfos.map {
- it.toEntity(folderInfoId)
+ appInfos.mapIndexed { index, appInfo ->
+ appInfo.toEntity(folderInfoId).copy(rank = index)
}.toList(),
)
}
@@ -70,7 +70,7 @@ class FolderService(val context: Context) : SafeCloseable {
title = folderWithItems.folder.title
}
- folderWithItems.items.forEach { itemEntity ->
+ folderWithItems.items.sortedBy { it.rank }.forEach { itemEntity ->
// Consider caching toItemInfo results if componentKey lookups are slow
// and items don't change frequently without folder data changing
toItemInfo(itemEntity.componentKey)?.let { appInfo ->
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt
index 0d94bdaad9..0bd50a582c 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt
@@ -36,34 +36,6 @@ import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
import app.lawnchair.util.App
import com.android.launcher3.model.data.AppInfo
-@Composable
-fun AppItem(
- app: App,
- onClick: (app: App) -> Unit,
- widget: (@Composable () -> Unit)? = null,
-) {
- AppItem(
- label = app.label,
- icon = app.icon,
- onClick = { onClick(app) },
- widget = widget,
- )
-}
-
-@Composable
-fun AppItem(
- appInfo: AppInfo,
- onClick: (appInfo: AppInfo) -> Unit,
- widget: (@Composable () -> Unit)? = null,
-) {
- AppItem(
- label = appInfo.title.toString(),
- icon = appInfo.bitmap.icon,
- onClick = { onClick(appInfo) },
- widget = widget,
- )
-}
-
@Composable
fun AppItem(
label: String,
@@ -71,11 +43,13 @@ fun AppItem(
onClick: () -> Unit,
modifier: Modifier = Modifier,
widget: (@Composable () -> Unit)? = null,
+ endWidget: (@Composable () -> Unit)? = null,
) {
AppItemLayout(
modifier = modifier
.clickable(onClick = onClick),
widget = widget,
+ endWidget = endWidget,
icon = {
Image(
bitmap = icon.asImageBitmap(),
@@ -87,6 +61,38 @@ fun AppItem(
)
}
+@Composable
+fun AppItem(
+ app: App,
+ onClick: (app: App) -> Unit,
+ widget: (@Composable () -> Unit)? = null,
+ endWidget: (@Composable () -> Unit)? = null,
+) {
+ AppItem(
+ label = app.label,
+ icon = app.icon,
+ onClick = { onClick(app) },
+ widget = widget,
+ endWidget = endWidget,
+ )
+}
+
+@Composable
+fun AppItem(
+ appInfo: AppInfo,
+ onClick: (appInfo: AppInfo) -> Unit,
+ widget: (@Composable () -> Unit)? = null,
+ endWidget: (@Composable () -> Unit)? = null,
+) {
+ AppItem(
+ label = appInfo.title.toString(),
+ icon = appInfo.bitmap.icon,
+ onClick = { onClick(appInfo) },
+ widget = widget,
+ endWidget = endWidget,
+ )
+}
+
@Composable
fun AppItemPlaceholder(
modifier: Modifier = Modifier,
@@ -125,6 +131,7 @@ private fun AppItemLayout(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
widget: (@Composable () -> Unit)? = null,
+ endWidget: (@Composable () -> Unit)? = null,
) {
PreferenceTemplate(
title = title,
@@ -136,6 +143,7 @@ private fun AppItemLayout(
}
icon()
},
+ endWidget = endWidget,
verticalPadding = 12.dp,
)
}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/DraggablePreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/DraggablePreference.kt
index 36cf72e93a..fcb4db60d1 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/DraggablePreference.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/DraggablePreference.kt
@@ -23,6 +23,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
@@ -55,6 +56,7 @@ fun DraggablePreferenceGroup(
defaultList: List,
onOrderChange: (List) -> Unit,
modifier: Modifier = Modifier,
+ onSettle: ((List) -> Unit)? = null,
itemContent: @Composable ReorderableScope.(
item: T,
index: Int,
@@ -62,7 +64,14 @@ fun DraggablePreferenceGroup(
onDraggingChange: (Boolean) -> Unit,
) -> Unit,
) {
- var localItems = items
+ var localItems by remember { mutableStateOf(items) }
+
+ LaunchedEffect(items) {
+ if (localItems != items) {
+ localItems = items
+ }
+ }
+
var isAnyDragging by remember { mutableStateOf(false) }
val color by animateColorAsState(
@@ -85,12 +94,15 @@ fun DraggablePreferenceGroup(
modifier = Modifier,
list = localItems,
onSettle = { fromIndex, toIndex ->
- localItems = localItems.toMutableList().apply {
+ val newItems = localItems.toMutableList().apply {
add(toIndex, removeAt(fromIndex))
- }.toList().also { newItems ->
- onOrderChange(newItems)
- isAnyDragging = false
+ }.toList()
+ localItems = newItems
+ onOrderChange(newItems)
+ if (onSettle != null) {
+ onSettle(newItems)
}
+ isAnyDragging = false
},
onMove = {
isAnyDragging = true
@@ -107,8 +119,20 @@ fun DraggablePreferenceGroup(
.a11yDrag(
index = index,
items = items,
- onMoveUp = { localItems = it },
- onMoveDown = { localItems = it },
+ onMoveUp = {
+ localItems = it
+ onOrderChange(it)
+ if (onSettle != null) {
+ onSettle(it)
+ }
+ },
+ onMoveDown = {
+ localItems = it
+ onOrderChange(it)
+ if (onSettle != null) {
+ onSettle(it)
+ }
+ },
),
) {
itemContent(
@@ -132,7 +156,11 @@ fun DraggablePreferenceGroup(
ExpandAndShrink(visible = localItems != defaultList) {
PreferenceGroup {
ClickablePreference(label = stringResource(id = R.string.action_reset)) {
- onOrderChange(defaultList)
+ val resetList = defaultList
+ onOrderChange(resetList)
+ if (onSettle != null) {
+ onSettle(resetList)
+ }
}
}
}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/SelectAppsForDrawerFolder.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/SelectAppsForDrawerFolder.kt
index 7d91f7c947..1b65f9ee60 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/SelectAppsForDrawerFolder.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/SelectAppsForDrawerFolder.kt
@@ -1,22 +1,20 @@
-package app.lawnchair.ui.preferences.destinations
+package app.lawnchair.ui.preferences.destinations
import android.content.Context
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.Crossfade
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Check
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
+import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -27,19 +25,19 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import app.lawnchair.data.folder.model.FolderViewModel
-import app.lawnchair.ui.OverflowMenu
import app.lawnchair.ui.preferences.LocalIsExpandedScreen
import app.lawnchair.ui.preferences.components.AppItem
import app.lawnchair.ui.preferences.components.AppItemPlaceholder
-import app.lawnchair.ui.preferences.components.layout.PreferenceDivider
-import app.lawnchair.ui.preferences.components.layout.PreferenceLazyColumn
-import app.lawnchair.ui.preferences.components.layout.PreferenceScaffold
+import app.lawnchair.ui.preferences.components.DragHandle
+import app.lawnchair.ui.preferences.components.DraggablePreferenceGroup
+import app.lawnchair.ui.preferences.components.layout.PreferenceLayoutLazyColumn
import app.lawnchair.ui.preferences.components.layout.preferenceGroupItems
import app.lawnchair.util.App
import app.lawnchair.util.appsState
import com.android.launcher3.R
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ComponentKey
@Composable
fun SelectAppsForDrawerFolder(
@@ -57,223 +55,128 @@ fun SelectAppsForDrawerFolder(
val folders by viewModel.folders.collectAsStateWithLifecycle()
val folderInfo by viewModel.folderInfo.collectAsStateWithLifecycle()
- var allFolderPackages by remember { mutableStateOf(emptySet()) }
- var selectedAppsInFolder by remember { mutableStateOf(setOf()) }
- var filterNonUniqueItems by remember { mutableStateOf(true) }
-
- LaunchedEffect(folders) {
- allFolderPackages = folders.flatMap { it.getContents() }
- .mapNotNull { it.targetPackage }
- .toSet()
- }
-
- LaunchedEffect(folderInfo) {
- selectedAppsInFolder = folderInfo?.getContents()?.toMutableSet() ?: emptySet()
- }
+ val selectedAppsInFolder = remember { mutableStateListOf() }
LaunchedEffect(folderInfoId) {
viewModel.setFolderInfo(folderInfoId, false)
}
val apps by appsState()
- val filteredApps = apps.filter { app ->
- if (filterNonUniqueItems) {
- !allFolderPackages.contains(app.key.componentName.packageName) || selectedAppsInFolder.map { it.targetPackage }.contains(app.key.componentName.packageName)
- } else {
- true
+ var isInitialLoad by remember { mutableStateOf(true) }
+
+ LaunchedEffect(folderInfo, apps) {
+ if (isInitialLoad && folderInfo != null && apps.isNotEmpty()) {
+ val currentContent = folderInfo!!.getContents()
+ val orderedApps = currentContent.sortedBy { it.rank }.mapNotNull { item ->
+ val key = ComponentKey(item.targetComponent, item.user)
+ apps.find { it.key == key }
+ }
+ selectedAppsInFolder.clear()
+ selectedAppsInFolder.addAll(orderedApps)
+ isInitialLoad = false
}
}
val loading = folderInfo == null && apps.isEmpty()
- PreferenceScaffold(
+ PreferenceLayoutLazyColumn(
label = if (loading) {
stringResource(R.string.loading)
} else {
stringResource(R.string.x_with_y_count, folderInfo?.title.toString(), selectedAppsInFolder.size)
},
modifier = modifier,
- actions = {
- if (!loading) {
- ListSortingOptions(
- originalList = apps,
- filteredList = selectedAppsInFolder,
- onUpdateList = { newSet ->
- selectedAppsInFolder = newSet
+ isExpandedScreen = LocalIsExpandedScreen.current,
+ ) {
+ if (loading) {
+ preferenceGroupItems(
+ count = 20,
+ isFirstChild = true,
+ dividerStartIndent = 40.dp,
+ ) {
+ AppItemPlaceholder()
+ }
+ } else {
+ item {
+ if (selectedAppsInFolder.isNotEmpty()) {
+ DraggablePreferenceGroup(
+ label = stringResource(R.string.selected_apps),
+ items = selectedAppsInFolder,
+ defaultList = selectedAppsInFolder,
+ onOrderChange = { newOrder ->
+ selectedAppsInFolder.clear()
+ selectedAppsInFolder.addAll(newOrder)
+ },
+ onSettle = { newOrder ->
+ // Update the database when drag settles
+ viewModel.updateFolderItems(
+ folderInfoId,
+ folderInfo?.title.toString(),
+ newOrder.map { it.toAppInfo(context) },
+ )
+ },
+ ) { app, _, _, onDraggingChange ->
+ val interactionSource = remember { MutableInteractionSource() }
+ AppItem(
+ app = app,
+ onClick = { },
+ widget = {
+ DragHandle(
+ scope = this,
+ interactionSource = interactionSource,
+ onDragStop = {
+ onDraggingChange(false)
+ },
+ )
+ },
+ endWidget = {
+ IconButton(onClick = {
+ val newList = selectedAppsInFolder.toMutableList()
+ newList.remove(app)
+ selectedAppsInFolder.clear()
+ selectedAppsInFolder.addAll(newList)
+ viewModel.updateFolderItems(
+ folderInfoId,
+ folderInfo?.title.toString(),
+ newList.map { it.toAppInfo(context) },
+ )
+ }) {
+ Icon(Icons.Rounded.Close, contentDescription = stringResource(R.string.delete_label))
+ }
+ },
+ )
+ }
+ }
+ }
+
+ val unselectedApps = apps.filter { !selectedAppsInFolder.contains(it) }
+
+ preferenceGroupItems(
+ items = unselectedApps,
+ heading = if (selectedAppsInFolder.isNotEmpty()) {
+ { stringResource(R.string.add_apps) }
+ } else {
+ null
+ },
+ isFirstChild = selectedAppsInFolder.isEmpty(),
+ dividerStartIndent = 40.dp,
+ ) { _, app ->
+ AppItem(
+ app = app,
+ onClick = {
+ selectedAppsInFolder.add(app)
viewModel.updateFolderItems(
folderInfoId,
folderInfo?.title.toString(),
- newSet.toList(),
+ selectedAppsInFolder.map { it.toAppInfo(context) },
)
},
- filterUniqueItems = filterNonUniqueItems,
- onToggleFilterUniqueItems = {
- filterNonUniqueItems = it
+ widget = {
+ Icon(Icons.Rounded.Add, contentDescription = null)
},
)
}
- },
- isExpandedScreen = LocalIsExpandedScreen.current,
- ) {
- Crossfade(targetState = loading, label = "") { isLoading ->
- if (isLoading) {
- PreferenceLazyColumn(it, enabled = false, state = rememberLazyListState()) {
- preferenceGroupItems(
- count = 20,
- isFirstChild = true,
- dividerStartIndent = 40.dp,
- ) {
- AppItemPlaceholder {
- Spacer(Modifier.width(24.dp))
- }
- }
- }
- } else {
- PreferenceLazyColumn(it, state = rememberLazyListState()) {
- preferenceGroupItems(
- filteredApps,
- isFirstChild = true,
- dividerStartIndent = 40.dp,
- ) { _, app ->
- key(app.toString()) {
- AppItem(
- app,
- onClick = {
- updateFolderItems(
- app = it,
- items = selectedAppsInFolder,
- context = context,
- onSetChange = { newSet ->
- selectedAppsInFolder = newSet
-
- viewModel.updateFolderItems(
- folderInfoId,
- folderInfo?.title.toString(),
- newSet.filterIsInstance().toList(),
- )
- },
- )
- },
- ) {
- Checkbox(
- checked = selectedAppsInFolder.any {
- val appInfo = it as? AppInfo
- appInfo?.targetPackage == app.key.componentName.packageName && appInfo.user == app.key.user
- },
- onCheckedChange = null,
- )
- }
- }
- }
- }
- }
}
}
}
-
-@Composable
-private fun ListSortingOptions(
- originalList: List,
- filteredList: Set,
- onUpdateList: (Set) -> Unit,
- filterUniqueItems: Boolean,
- onToggleFilterUniqueItems: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
-) {
- val context = LocalContext.current
- OverflowMenu(modifier) {
- val originalListPackageNames = originalList
- .map { it.key.componentName.packageName }
- DropdownMenuItem(
- onClick = {
- val inverseSelectionPackageNames = originalListPackageNames
- .filter { items ->
- !filteredList.map { it.targetPackage }.contains(items)
- }
- .toSet()
-
- val inverseSelection = originalList
- .filter {
- inverseSelectionPackageNames.contains(it.key.componentName.packageName)
- }
- .map {
- it.toAppInfo(context)
- }
- .toSet()
-
- onUpdateList(inverseSelection)
- hideMenu()
- },
- text = {
- Text(stringResource(R.string.inverse_selection))
- },
- )
-
- val selectedAll = originalListPackageNames == filteredList.map { it.targetPackage }
- DropdownMenuItem(
- onClick = {
- onUpdateList(
- if (selectedAll) {
- emptySet()
- } else {
- originalList.map { app ->
- app.toAppInfo(context)
- }.toSet()
- },
- )
- hideMenu()
- },
- text = {
- Text(
- stringResource(if (selectedAll) R.string.deselect_all else R.string.select_all),
- )
- },
- )
- DropdownMenuItem(
- onClick = {
- onToggleFilterUniqueItems(!filterUniqueItems)
- hideMenu()
- },
- trailingIcon = {
- if (filterUniqueItems) {
- Icon(Icons.Rounded.Check, contentDescription = null)
- }
- },
- text = {
- Text(stringResource(R.string.folders_filter_duplicates))
- },
- )
- PreferenceDivider(modifier = Modifier.padding(vertical = 8.dp))
- DropdownMenuItem(
- onClick = {
- onUpdateList(
- emptySet(),
- )
- },
- text = {
- Text(stringResource(R.string.action_reset))
- },
- )
- }
-}
-
-fun updateFolderItems(
- app: App,
- items: Set,
- context: Context,
- onSetChange: (Set) -> Unit,
-) {
- val newSet = items.toMutableSet().apply {
- val isChecked = any { it is AppInfo && it.targetPackage == app.key.componentName.packageName && it.user == app.key.user }
- if (isChecked) {
- removeIf { it is AppInfo && it.targetPackage == app.key.componentName.packageName && it.user == app.key.user }
- } else {
- add(
- app.toAppInfo(context),
- )
- }
- }
-
- onSetChange(newSet)
-}