Feat: Shopping List views enhanced with search, item management, and UI improvements.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -7,15 +9,21 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.material3.Button // New import
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField // New import
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.getValue // New import
|
||||
import androidx.compose.runtime.mutableStateOf // New import
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue // New import
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -34,61 +42,129 @@ fun ShoppingListDetailScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var newItemName by remember { mutableStateOf("") }
|
||||
val showCompletedItems by viewModel.showCompletedItems.collectAsState() // New: collect showCompletedItems state
|
||||
|
||||
// Fetch the specific shopping list with items
|
||||
val shoppingListWithItems by viewModel.getShoppingListWithItemsStream(listId ?: 0).collectAsState(initial = null)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (shoppingListWithItems == null) { // Changed from ?: run { ... }
|
||||
Text(
|
||||
text = stringResource(R.string.shopping_list_not_found),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
// Optionally, a button to go back
|
||||
// Button(onClick = onBack) {
|
||||
// Text(stringResource(R.string.back))
|
||||
// }
|
||||
} else {
|
||||
val list = shoppingListWithItems!! // Safe to use !! here because we checked for null
|
||||
Text(
|
||||
text = list.shoppingList.name,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
if (list.items.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_items_in_list))
|
||||
Box(modifier = modifier.fillMaxSize()) { // Use Box for button placement
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (shoppingListWithItems == null) {
|
||||
Text(
|
||||
text = stringResource(R.string.shopping_list_not_found),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(list.items) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { isChecked ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = isChecked))
|
||||
}
|
||||
val list = shoppingListWithItems!!
|
||||
Text(
|
||||
text = list.shoppingList.name,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
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))
|
||||
Button(
|
||||
onClick = {
|
||||
if (newItemName.isNotBlank()) {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(
|
||||
ShoppingListItem(name = newItemName, listId = list.shoppingList.id)
|
||||
)
|
||||
newItemName = "" // Clear input field
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = newItemName.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(R.string.add_item)) // Reusing string, should be "Add"
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (list.items.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_items_in_list))
|
||||
} else {
|
||||
val filteredItems = if (showCompletedItems) {
|
||||
list.items
|
||||
} else {
|
||||
list.items.filter { !it.isChecked } // Filter out checked items
|
||||
}
|
||||
|
||||
if (filteredItems.isEmpty() && !showCompletedItems) {
|
||||
Text(text = stringResource(R.string.no_uncompleted_items))
|
||||
} else if (filteredItems.isEmpty() && showCompletedItems) {
|
||||
Text(text = stringResource(R.string.no_items_in_list))
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(filteredItems) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { isChecked ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = isChecked))
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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),
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(onClick = { viewModel.toggleShowCompletedItems() }) {
|
||||
Text(text = if (showCompletedItems) stringResource(R.string.hide_completed_items) else stringResource(R.string.show_completed_items))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
listId?.let { viewModel.deleteCompletedItems(it) }
|
||||
}
|
||||
},
|
||||
enabled = list.items.any { it.isChecked } // Enable only if there are checked items
|
||||
) {
|
||||
Text(stringResource(R.string.remove_completed_items))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
@@ -20,6 +23,7 @@ import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -54,6 +58,7 @@ fun ShoppingListsScreen(
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val listDetails by viewModel.listDetails.collectAsState()
|
||||
val itemDetails by viewModel.itemDetails.collectAsState()
|
||||
val searchQuery by viewModel.searchQuery.collectAsState() // New: collect search query
|
||||
|
||||
var showListDialog by remember { mutableStateOf(false) }
|
||||
var showItemDialog by remember { mutableStateOf(false) }
|
||||
@@ -77,6 +82,13 @@ fun ShoppingListsScreen(
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
OutlinedTextField( // New: Search input field
|
||||
value = searchQuery,
|
||||
onValueChange = { viewModel.updateSearchQuery(it) },
|
||||
label = { Text(stringResource(R.string.search_list_hint)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
|
||||
)
|
||||
if (uiState.shoppingLists.isEmpty()) {
|
||||
Text(text = stringResource(R.string.no_shopping_lists_yet))
|
||||
} else {
|
||||
@@ -93,25 +105,6 @@ fun ShoppingListsScreen(
|
||||
viewModel.deleteList(it.shoppingList)
|
||||
}
|
||||
},
|
||||
onAddItem = {
|
||||
currentSelectedListId = it.shoppingList.id
|
||||
viewModel.resetItemDetails(it.shoppingList.id)
|
||||
showItemDialog = true
|
||||
},
|
||||
onEditItem = {
|
||||
viewModel.updateItemDetails(it)
|
||||
showItemDialog = true
|
||||
},
|
||||
onToggleItemChecked = {
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(it.copy(isChecked = !it.isChecked))
|
||||
}
|
||||
},
|
||||
onDeleteItem = {
|
||||
coroutineScope.launch {
|
||||
viewModel.deleteItem(it)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable { onListClick(listWithItems.shoppingList.id) } // Make card clickable
|
||||
)
|
||||
}
|
||||
@@ -158,10 +151,6 @@ fun ShoppingListCard(
|
||||
listWithItems: ShoppingListWithItems,
|
||||
onEditList: (ShoppingListWithItems) -> Unit,
|
||||
onDeleteList: (ShoppingListWithItems) -> Unit,
|
||||
onAddItem: (ShoppingListWithItems) -> Unit,
|
||||
onEditItem: (ShoppingListItem) -> Unit,
|
||||
onToggleItemChecked: (ShoppingListItem) -> Unit,
|
||||
onDeleteItem: (ShoppingListItem) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
@@ -180,39 +169,13 @@ fun ShoppingListCard(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(onClick = { onEditList(listWithItems) }) {
|
||||
Text(stringResource(R.string.edit))
|
||||
IconButton(onClick = { onEditList(listWithItems) }) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit_list))
|
||||
}
|
||||
TextButton(onClick = { onDeleteList(listWithItems) }) {
|
||||
Text(stringResource(R.string.delete))
|
||||
IconButton(onClick = { onDeleteList(listWithItems) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete_list))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = stringResource(R.string.items_in_list))
|
||||
listWithItems.items.forEach { item ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { onToggleItemChecked(item) }
|
||||
)
|
||||
Text(
|
||||
text = item.name,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable { onEditItem(item) }
|
||||
)
|
||||
TextButton(onClick = { onDeleteItem(item) }) {
|
||||
Text(stringResource(R.string.delete))
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(onClick = { onAddItem(listWithItems) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.add_item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,31 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine // New import
|
||||
import kotlinx.coroutines.flow.filter // New import
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository) : ViewModel() {
|
||||
|
||||
val uiState: StateFlow<ShoppingListsUiState> = noteshopRepository.getAllShoppingListsWithItemsStream().map { ShoppingListsUiState(it) }
|
||||
private val _searchQuery = MutableStateFlow("") // New search query state
|
||||
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
|
||||
|
||||
private val _showCompletedItems = MutableStateFlow(false) // New state for showing completed items
|
||||
val showCompletedItems: StateFlow<Boolean> = _showCompletedItems.asStateFlow()
|
||||
|
||||
val uiState: StateFlow<ShoppingListsUiState> = combine(
|
||||
noteshopRepository.getAllShoppingListsWithItemsStream(),
|
||||
searchQuery
|
||||
) { allLists, query ->
|
||||
val filteredLists = if (query.isBlank()) {
|
||||
allLists
|
||||
} else {
|
||||
allLists.filter { it.shoppingList.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
ShoppingListsUiState(filteredLists)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
|
||||
@@ -91,6 +109,22 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
return noteshopRepository.getShoppingListWithItemsStream(listId)
|
||||
}
|
||||
|
||||
fun updateSearchQuery(query: String) { // New function to update search query
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
fun toggleShowCompletedItems() { // New function to toggle show completed items
|
||||
_showCompletedItems.value = !_showCompletedItems.value
|
||||
}
|
||||
|
||||
suspend fun deleteCompletedItems(listId: Int) { // New function to delete completed items
|
||||
noteshopRepository.getShoppingListWithItemsStream(listId).collect { listWithItems ->
|
||||
listWithItems?.items?.filter { it.isChecked }?.forEach { item ->
|
||||
noteshopRepository.deleteShoppingListItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
}
|
||||
|
||||
@@ -24,7 +24,15 @@
|
||||
<string name="item_name">Artikelname</string>
|
||||
<string name="item_checked">Abgehakt</string>
|
||||
<string name="edit">Bearbeiten</string>
|
||||
<string name="edit_list">Liste bearbeiten</string>
|
||||
<string name="delete_list">Liste löschen</string>
|
||||
<string name="menu_shopping_list_detail">Einkaufslisten-Details</string>
|
||||
<string name="no_items_in_list">Noch keine Artikel in dieser Liste.</string>
|
||||
<string name="shopping_list_not_found">Einkaufsliste nicht gefunden.</string>
|
||||
<string name="search_list_hint">Listen suchen...</string>
|
||||
<string name="add_item_hint">Artikelname</string>
|
||||
<string name="show_completed_items">Erledigte anzeigen</string>
|
||||
<string name="hide_completed_items">Erledigte ausblenden</string>
|
||||
<string name="remove_completed_items">Erledigte entfernen</string>
|
||||
<string name="no_uncompleted_items">Keine unerledigten Artikel in dieser Liste.</string>
|
||||
</resources>
|
||||
@@ -24,7 +24,15 @@
|
||||
<string name="item_name">Item Name</string>
|
||||
<string name="item_checked">Checked</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="edit_list">Edit list</string>
|
||||
<string name="delete_list">Delete list</string>
|
||||
<string name="menu_shopping_list_detail">Shopping List Details</string>
|
||||
<string name="no_items_in_list">No items in this list yet.</string>
|
||||
<string name="shopping_list_not_found">Shopping list not found.</string>
|
||||
<string name="search_list_hint">Search lists...</string>
|
||||
<string name="add_item_hint">Item name</string>
|
||||
<string name="show_completed_items">Show completed</string>
|
||||
<string name="hide_completed_items">Hide completed</string>
|
||||
<string name="remove_completed_items">Remove completed</string>
|
||||
<string name="no_uncompleted_items">No uncompleted items in this list.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user