feat(ui): Implement reorder mode for shopping list items
Implements a "reorder mode" for the shopping list detail screen to resolve the conflict between scrolling and drag-to-reorder gestures. - A long press on a list item now activates reorder mode. - In this mode, a drag handle is shown on each item, and list items can be reordered. Regular clicks for checking items are disabled. - The back button deactivates the reorder mode. - Gesture handling has been consolidated to correctly detect both taps and long presses on list items.
This commit is contained in:
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
@@ -110,6 +111,9 @@ fun AppShell(
|
||||
var currentScreen: Screen by remember { mutableStateOf(Screen.ShoppingLists) }
|
||||
var selectedListId: Int? by remember { mutableStateOf(null) }
|
||||
|
||||
var showListDialog by remember { mutableStateOf(false) }
|
||||
val listDetails by shoppingListsViewModel.listDetails.collectAsState()
|
||||
|
||||
val navigationItems = listOf(
|
||||
Screen.ShoppingLists,
|
||||
Screen.Notes
|
||||
@@ -174,42 +178,14 @@ fun AppShell(
|
||||
TopAppBar(
|
||||
title = { Text(topBarTitle) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { currentScreen = Screen.ShoppingLists; selectedListId = null }) {
|
||||
IconButton(onClick = {
|
||||
shoppingListsViewModel.disableReorderMode()
|
||||
currentScreen = Screen.ShoppingLists; selectedListId = null
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { scope.launch { selectedItem?.let { shoppingListsViewModel.moveItemUp(selectedListId ?: 0, it) }; shoppingListsViewModel.onSelectItem(null) } }, enabled = selectedItem != null,
|
||||
modifier = Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
scope.launch { selectedItem?.let { shoppingListsViewModel.moveItemToTop(selectedListId ?: 0, it) } }
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.ArrowUpward, contentDescription = stringResource(R.string.move_item_up))
|
||||
}
|
||||
IconButton(onClick = { scope.launch { selectedItem?.let { shoppingListsViewModel.moveItemDown(selectedListId ?: 0, it) } }; shoppingListsViewModel.onSelectItem(null) }, enabled = selectedItem != null,
|
||||
modifier = Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
scope.launch { selectedItem?.let { shoppingListsViewModel.moveItemToBottom(selectedListId ?: 0, it) } }
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.ArrowDownward, contentDescription = stringResource(R.string.move_item_down))
|
||||
}
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showMenu = !showMenu }, enabled = selectedItem != null) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.more_options))
|
||||
}
|
||||
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.rename)) }, onClick = { shoppingListsViewModel.onShowRenameDialog(true); showMenu = false })
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.change_quantity)) }, onClick = { shoppingListsViewModel.onShowQuantityDialog(true); showMenu = false })
|
||||
}
|
||||
},
|
||||
actions = {},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
@@ -222,12 +198,21 @@ fun AppShell(
|
||||
Icon(imageVector = Icons.Default.Menu, contentDescription = stringResource(id = R.string.menu_open))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
actions = {
|
||||
if (currentScreen == Screen.ShoppingLists) {
|
||||
IconButton(onClick = {
|
||||
shoppingListsViewModel.resetListDetails()
|
||||
showListDialog = true
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_shopping_list))
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
if (currentScreen == Screen.ShoppingLists) {
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
@@ -268,6 +253,22 @@ fun AppShell(
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
if (showListDialog) {
|
||||
de.lxtools.noteshop.ui.shopping.ShoppingListInputDialog(
|
||||
listDetails = listDetails,
|
||||
onValueChange = shoppingListsViewModel::updateListDetails,
|
||||
onSave = {
|
||||
scope.launch {
|
||||
shoppingListsViewModel.saveList()
|
||||
showListDialog = false
|
||||
shoppingListsViewModel.resetListDetails()
|
||||
}
|
||||
},
|
||||
onDismiss = { showListDialog = false },
|
||||
isNewList = listDetails.id == 0
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -281,6 +282,10 @@ fun AppShell(
|
||||
onListClick = { listId ->
|
||||
selectedListId = listId
|
||||
currentScreen = Screen.ShoppingListDetail
|
||||
},
|
||||
onEditList = { listWithItems ->
|
||||
shoppingListsViewModel.updateListDetails(listWithItems.shoppingList)
|
||||
showListDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -70,6 +72,7 @@ fun ShoppingListDetailScreen(
|
||||
val selectedItem by viewModel.selectedItem.collectAsState()
|
||||
val showRenameDialog by viewModel.showRenameDialog.collectAsState()
|
||||
val showQuantityDialog by viewModel.showQuantityDialog.collectAsState()
|
||||
val isReorderMode by viewModel.isReorderMode.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
@@ -152,14 +155,25 @@ fun ShoppingListDetailScreen(
|
||||
) {
|
||||
items(filteredItems, { it.id }) { item ->
|
||||
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
||||
val elevation = animateDpAsState(if (isDragging) 4.dp else 0.dp, label = "elevation")
|
||||
val elevation = animateDpAsState(if (isDragging) 4.dp else 1.dp, label = "elevation")
|
||||
val scale = animateFloatAsState(if (isDragging) 1.05f else 1.0f, label = "scale")
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.draggableHandle()
|
||||
.pointerInput(isReorderMode) {
|
||||
if (!isReorderMode) {
|
||||
detectTapGestures(
|
||||
onLongPress = { viewModel.enableReorderMode() },
|
||||
onTap = {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = !item.isChecked))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.graphicsLayer {
|
||||
scaleX = scale.value
|
||||
scaleY = scale.value
|
||||
@@ -169,14 +183,14 @@ fun ShoppingListDetailScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { // The check/uncheck logic
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = !item.isChecked))
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (isReorderMode) {
|
||||
IconButton(modifier = Modifier.draggableHandle(), onClick = {}) {
|
||||
Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = if (item.quantity != null) "${item.name} (${item.quantity})" else item.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
|
||||
@@ -62,98 +62,44 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
fun ShoppingListsScreen(
|
||||
viewModel: ShoppingListsViewModel = viewModel(factory = AppViewModelProvider.Factory),
|
||||
modifier: Modifier = Modifier,
|
||||
onListClick: (Int) -> Unit // New parameter
|
||||
onListClick: (Int) -> Unit,
|
||||
onEditList: (ShoppingListWithItems) -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val listDetails by viewModel.listDetails.collectAsState()
|
||||
val itemDetails by viewModel.itemDetails.collectAsState()
|
||||
|
||||
var showListDialog by remember { mutableStateOf(false) }
|
||||
var showItemDialog by remember { mutableStateOf(false) }
|
||||
var currentSelectedListId by remember { mutableStateOf(0) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
viewModel.resetListDetails()
|
||||
showListDialog = true
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_shopping_list))
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
if (uiState.shoppingLists.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_shopping_lists_yet))
|
||||
} else {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
viewModel.moveList(from.index, to.index)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
if (uiState.shoppingLists.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_shopping_lists_yet))
|
||||
} else {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
viewModel.moveList(from.index, to.index)
|
||||
}
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(uiState.shoppingLists, { it.shoppingList.id }) { listWithItems ->
|
||||
ReorderableItem(reorderableLazyListState, key = listWithItems.shoppingList.id) { isDragging ->
|
||||
ShoppingListCard(
|
||||
listWithItems = listWithItems,
|
||||
onEditList = {
|
||||
viewModel.updateListDetails(it.shoppingList)
|
||||
showListDialog = true
|
||||
},
|
||||
onDeleteList = {
|
||||
coroutineScope.launch {
|
||||
viewModel.deleteList(it.shoppingList)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable { onListClick(listWithItems.shoppingList.id) },
|
||||
scope = this,
|
||||
isDragging = isDragging
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(uiState.shoppingLists, { it.shoppingList.id }) { listWithItems ->
|
||||
ReorderableItem(reorderableLazyListState, key = listWithItems.shoppingList.id) { isDragging ->
|
||||
ShoppingListCard(
|
||||
listWithItems = listWithItems,
|
||||
onEditList = onEditList,
|
||||
onDeleteList = {
|
||||
coroutineScope.launch {
|
||||
viewModel.deleteList(it.shoppingList)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable { onListClick(listWithItems.shoppingList.id) },
|
||||
scope = this,
|
||||
isDragging = isDragging
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showListDialog) {
|
||||
ShoppingListInputDialog(
|
||||
listDetails = listDetails,
|
||||
onValueChange = viewModel::updateListDetails,
|
||||
onSave = {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveList()
|
||||
showListDialog = false
|
||||
viewModel.resetListDetails()
|
||||
}
|
||||
},
|
||||
onDismiss = { showListDialog = false },
|
||||
isNewList = listDetails.id == 0
|
||||
)
|
||||
}
|
||||
|
||||
if (showItemDialog) {
|
||||
ShoppingListItemInputDialog(
|
||||
itemDetails = itemDetails,
|
||||
onValueChange = viewModel::updateItemDetails,
|
||||
onSave = {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(itemDetails.toShoppingListItem())
|
||||
showItemDialog = false
|
||||
viewModel.resetItemDetails(currentSelectedListId)
|
||||
}
|
||||
},
|
||||
onDismiss = { showItemDialog = false },
|
||||
isNewItem = itemDetails.id == 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
private val _showQuantityDialog = MutableStateFlow(false)
|
||||
val showQuantityDialog: StateFlow<Boolean> = _showQuantityDialog.asStateFlow()
|
||||
|
||||
private val _isReorderMode = MutableStateFlow(false)
|
||||
val isReorderMode: StateFlow<Boolean> = _isReorderMode.asStateFlow()
|
||||
|
||||
fun onNewItemNameChange(name: String) {
|
||||
_newItemName.value = name
|
||||
}
|
||||
@@ -54,6 +57,14 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
_showQuantityDialog.value = show
|
||||
}
|
||||
|
||||
fun enableReorderMode() {
|
||||
_isReorderMode.value = true
|
||||
}
|
||||
|
||||
fun disableReorderMode() {
|
||||
_isReorderMode.value = false
|
||||
}
|
||||
|
||||
val uiState: StateFlow<ShoppingListsUiState> = combine(
|
||||
noteshopRepository.getAllShoppingListsWithItemsStream(),
|
||||
searchQuery
|
||||
|
||||
Reference in New Issue
Block a user