feat: Enhance shopping list detail screen
- Rework layout to use a BottomAppBar for actions. - Fix keyboard overlapping issue by removing fixed height and using imePadding. - Prevent adding duplicate items to the shopping list. - Add search-as-you-type functionality to show completed items as suggestions.
This commit is contained in:
5
GEMINI.md
Normal file
5
GEMINI.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Gemini Project Configuration
|
||||
|
||||
## Language
|
||||
Please respond in German.
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
package de.lxtools.noteshop.ui.shopping
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement // New import
|
||||
import androidx.compose.foundation.layout.Box // New import
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding // New import
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.LooksOne
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon // New import
|
||||
import androidx.compose.material3.IconButton // New import
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField // New import
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue // New import
|
||||
import androidx.compose.runtime.mutableStateOf // New import
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue // New import
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -47,15 +51,110 @@ fun ShoppingListDetailScreen(
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var newItemName by remember { mutableStateOf("") }
|
||||
val showCompletedItems by viewModel.showCompletedItems.collectAsState() // New: collect showCompletedItems state
|
||||
val showCompletedItems by viewModel.showCompletedItems.collectAsState()
|
||||
|
||||
// Fetch the specific shopping list with items
|
||||
val shoppingListWithItems by viewModel.getShoppingListWithItemsStream(listId ?: 0).collectAsState(initial = null)
|
||||
val shoppingListWithItems by viewModel.getShoppingListWithItemsStream(listId ?: 0)
|
||||
.collectAsState(initial = null)
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) { // Use Box for button placement
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
if (shoppingListWithItems != null) {
|
||||
val list = shoppingListWithItems!!
|
||||
BottomAppBar(
|
||||
modifier = Modifier.imePadding()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton( // Default Add Button
|
||||
onClick = {
|
||||
if (newItemName.isNotBlank()) {
|
||||
coroutineScope.launch {
|
||||
val existingItemNames = list.items.map { it.name.trim().lowercase() }
|
||||
val itemsToAdd = if (newItemName.contains(',')) {
|
||||
newItemName.split(',').map { it.trim() }.filter { it.isNotBlank() && !existingItemNames.contains(it.lowercase()) }
|
||||
} else {
|
||||
if (!existingItemNames.contains(newItemName.trim().lowercase())) {
|
||||
listOf(newItemName.trim())
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
itemsToAdd.forEach { name ->
|
||||
viewModel.saveShoppingListItem(
|
||||
ShoppingListItem(
|
||||
name = name,
|
||||
listId = list.shoppingList.id
|
||||
)
|
||||
)
|
||||
}
|
||||
newItemName = "" // Clear input field
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = newItemName.isNotBlank()
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||
contentDescription = stringResource(R.string.add_item_icon_desc)
|
||||
)
|
||||
}
|
||||
IconButton( // Simple Mode Add Button
|
||||
onClick = {
|
||||
if (newItemName.isNotBlank()) {
|
||||
coroutineScope.launch {
|
||||
val existingItemNames = list.items.map { it.name.trim().lowercase() }
|
||||
if (!existingItemNames.contains(newItemName.trim().lowercase())) {
|
||||
viewModel.saveShoppingListItem(
|
||||
ShoppingListItem(name = newItemName.trim(), listId = list.shoppingList.id)
|
||||
)
|
||||
}
|
||||
newItemName = "" // Clear input field
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = newItemName.isNotBlank()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.LooksOne,
|
||||
contentDescription = stringResource(R.string.add_item_simple_icon_desc)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { viewModel.toggleShowCompletedItems() }) {
|
||||
Icon(
|
||||
imageVector = if (showCompletedItems) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
contentDescription = if (showCompletedItems) stringResource(R.string.hide_completed_icon_desc) else stringResource(
|
||||
R.string.show_completed_icon_desc
|
||||
)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
listId?.let { viewModel.deleteCompletedItems(it) }
|
||||
}
|
||||
},
|
||||
enabled = list.items.any { it.isChecked }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.remove_completed_icon_desc)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (shoppingListWithItems == null) {
|
||||
@@ -73,57 +172,44 @@ fun ShoppingListDetailScreen(
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newItemName,
|
||||
onValueChange = { newItemName = it },
|
||||
label = { Text(stringResource(R.string.add_item_hint)) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton( // Default Add Button
|
||||
onClick = {
|
||||
if (newItemName.isNotBlank()) {
|
||||
coroutineScope.launch {
|
||||
val itemsToAdd = if (newItemName.contains(',')) {
|
||||
newItemName.split(',').map { it.trim() }.filter { it.isNotBlank() }
|
||||
} else {
|
||||
listOf(newItemName.trim())
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = newItemName,
|
||||
onValueChange = { newItemName = it },
|
||||
label = { Text(stringResource(R.string.add_item_hint)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
itemsToAdd.forEach { name ->
|
||||
viewModel.saveShoppingListItem(
|
||||
ShoppingListItem(name = name, listId = list.shoppingList.id)
|
||||
)
|
||||
}
|
||||
newItemName = "" // Clear input field
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = newItemName.isNotBlank()
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.PlaylistAdd, contentDescription = stringResource(R.string.add_item_icon_desc))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton( // Simple Mode Add Button
|
||||
onClick = {
|
||||
if (newItemName.isNotBlank()) {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(
|
||||
ShoppingListItem(name = newItemName.trim(), listId = list.shoppingList.id)
|
||||
if (newItemName.isNotBlank()) {
|
||||
val suggestions = shoppingListWithItems?.items?.filter {
|
||||
it.name.contains(newItemName, ignoreCase = true) && it.isChecked
|
||||
} ?: emptyList()
|
||||
|
||||
if (suggestions.isNotEmpty()) {
|
||||
LazyColumn(modifier = Modifier.heightIn(max = 150.dp)) {
|
||||
items(suggestions) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = false))
|
||||
newItemName = ""
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
newItemName = "" // Clear input field
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = newItemName.isNotBlank()
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.PlaylistAdd, contentDescription = stringResource(R.string.add_item_simple_icon_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (list.items.isEmpty()) {
|
||||
@@ -152,7 +238,11 @@ fun ShoppingListDetailScreen(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { isChecked ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = isChecked))
|
||||
viewModel.saveShoppingListItem(
|
||||
item.copy(
|
||||
isChecked = isChecked
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -168,37 +258,5 @@ fun ShoppingListDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New: Buttons at bottom right - direct child of Box
|
||||
if (shoppingListWithItems != null) { // Only show buttons if list exists
|
||||
val list = shoppingListWithItems!! // Re-declare list for scope
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp)
|
||||
.imePadding(), // Apply imePadding here
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
IconButton(onClick = { viewModel.toggleShowCompletedItems() }) {
|
||||
Icon(
|
||||
imageVector = if (showCompletedItems) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
|
||||
|
||||
contentDescription = if (showCompletedItems) stringResource(R.string.hide_completed_icon_desc) else stringResource(R.string.show_completed_icon_desc)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
listId?.let { viewModel.deleteCompletedItems(it) }
|
||||
}
|
||||
},
|
||||
enabled = list.items.any { it.isChecked }
|
||||
) {
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.remove_completed_icon_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user