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:
2025-10-11 11:42:05 +02:00
parent 1f06eae886
commit 52da1101d2
2 changed files with 155 additions and 92 deletions

5
GEMINI.md Normal file
View File

@@ -0,0 +1,5 @@
# Gemini Project Configuration
## Language
Please respond in German.

View File

@@ -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))
}
}
}
}
}