feat: Implement drag-and-drop reordering for shopping list items and externalize UI strings
This commit is contained in:
@@ -10,7 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
@Database(
|
||||
entities = [Note::class, ShoppingList::class, ShoppingListItem::class],
|
||||
version = 3, // Incremented version
|
||||
version = 4, // Incremented version
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -31,7 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
AppDatabase::class.java,
|
||||
"noteshop_database"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
// return instance
|
||||
@@ -50,5 +50,11 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
db.execSQL("ALTER TABLE shopping_lists ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE shopping_list_items ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,11 +71,21 @@ interface NoteshopRepository {
|
||||
*/
|
||||
suspend fun deleteShoppingListItem(item: ShoppingListItem)
|
||||
|
||||
/**
|
||||
* Update multiple shopping list items in the data source
|
||||
*/
|
||||
suspend fun updateShoppingListItems(items: List<ShoppingListItem>)
|
||||
|
||||
/**
|
||||
* Get the number of shopping lists.
|
||||
*/
|
||||
suspend fun getListsCount(): Int
|
||||
|
||||
/**
|
||||
* Get the number of items in a specific shopping list.
|
||||
*/
|
||||
suspend fun getItemsCount(listId: Int): Int
|
||||
|
||||
}
|
||||
|
||||
class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppingListDao: ShoppingListDao) : NoteshopRepository {
|
||||
@@ -107,4 +117,8 @@ class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppi
|
||||
|
||||
override suspend fun getListsCount(): Int = shoppingListDao.getListsCount()
|
||||
|
||||
override suspend fun getItemsCount(listId: Int): Int = shoppingListDao.getItemsCount(listId)
|
||||
|
||||
override suspend fun updateShoppingListItems(items: List<ShoppingListItem>) = shoppingListDao.updateItems(items)
|
||||
|
||||
}
|
||||
@@ -35,6 +35,12 @@ interface ShoppingListDao {
|
||||
@Delete
|
||||
suspend fun deleteItem(item: ShoppingListItem)
|
||||
|
||||
@Update
|
||||
suspend fun updateItems(items: List<ShoppingListItem>)
|
||||
|
||||
@Query("SELECT COUNT(*) FROM shopping_list_items WHERE list_id = :listId")
|
||||
suspend fun getItemsCount(listId: Int): Int
|
||||
|
||||
// --- Methods for combined data ---
|
||||
@Transaction
|
||||
@Query("SELECT * FROM shopping_lists ORDER BY displayOrder ASC")
|
||||
|
||||
@@ -24,5 +24,6 @@ data class ShoppingListItem(
|
||||
@ColumnInfo(name = "is_checked")
|
||||
val isChecked: Boolean = false,
|
||||
@ColumnInfo(name = "list_id", index = true)
|
||||
val listId: Int
|
||||
val listId: Int,
|
||||
val displayOrder: Int = 0
|
||||
)
|
||||
@@ -19,6 +19,7 @@ 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.DragHandle
|
||||
import androidx.compose.material.icons.filled.LooksOne
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
@@ -49,332 +50,655 @@ import androidx.compose.ui.unit.dp
|
||||
import de.lxtools.noteshop.R
|
||||
import de.lxtools.noteshop.data.ShoppingListItem
|
||||
import kotlinx.coroutines.launch
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
|
||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
|
||||
fun ShoppingListDetailScreen(
|
||||
|
||||
listId: Int?,
|
||||
|
||||
viewModel: ShoppingListsViewModel,
|
||||
|
||||
modifier: Modifier = Modifier
|
||||
|
||||
) {
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var newItemName by remember { mutableStateOf("") }
|
||||
|
||||
val showCompletedItems by viewModel.showCompletedItems.collectAsState()
|
||||
|
||||
|
||||
|
||||
val shoppingListWithItems by viewModel.getShoppingListWithItemsStream(listId ?: 0)
|
||||
|
||||
.collectAsState(initial = null)
|
||||
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
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 = list.items.any { it.isChecked }
|
||||
|
||||
enabled = newItemName.isNotBlank()
|
||||
|
||||
) {
|
||||
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.remove_completed_icon_desc)
|
||||
|
||||
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) {
|
||||
|
||||
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 {
|
||||
|
||||
val list = shoppingListWithItems!!
|
||||
|
||||
Text(
|
||||
|
||||
text = list.shoppingList.name,
|
||||
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
||||
OutlinedTextField(
|
||||
|
||||
value = newItemName,
|
||||
|
||||
onValueChange = { newItemName = it },
|
||||
|
||||
label = { Text(stringResource(R.string.add_item_hint)) },
|
||||
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
||||
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 + " (" + stringResource(R.string.completed) + ")",
|
||||
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
|
||||
modifier = Modifier.weight(1f)
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
val filteredItems = if (showCompletedItems) {
|
||||
|
||||
list.items
|
||||
|
||||
} else {
|
||||
|
||||
list.items.filter { !it.isChecked } // Filter out checked items
|
||||
|
||||
}.sortedBy { it.displayOrder }
|
||||
|
||||
|
||||
|
||||
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 ->
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
var showQuantityDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showRenameDialog) {
|
||||
RenameItemDialog(
|
||||
item = item,
|
||||
onDismiss = { showRenameDialog = false },
|
||||
onRename = { newName ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(name = newName))
|
||||
}
|
||||
showRenameDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showQuantityDialog) {
|
||||
QuantityDialog(
|
||||
item = item,
|
||||
onDismiss = { showQuantityDialog = false },
|
||||
onSetQuantity = { quantity ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(quantity = quantity))
|
||||
}
|
||||
showQuantityDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
showMenu = true
|
||||
}
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { isChecked ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = isChecked))
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = if (item.quantity != null) "${item.name} (${item.quantity})" else item.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Umbenennen") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showRenameDialog = true
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Menge ändern") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showQuantityDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val state = rememberReorderableLazyListState(onMove = { from, to ->
|
||||
|
||||
listId?.let { viewModel.moveItem(it, from.index, to.index) }
|
||||
|
||||
})
|
||||
|
||||
LazyColumn(
|
||||
|
||||
state = state.listState,
|
||||
|
||||
modifier = Modifier.reorderable(state)
|
||||
|
||||
) {
|
||||
|
||||
items(filteredItems, { it.id }) { item ->
|
||||
|
||||
ReorderableItem(state, key = item.id) {
|
||||
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var showQuantityDialog by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
|
||||
if (showRenameDialog) {
|
||||
|
||||
RenameItemDialog(
|
||||
|
||||
item = item,
|
||||
|
||||
onDismiss = { showRenameDialog = false },
|
||||
|
||||
onRename = { newName ->
|
||||
|
||||
coroutineScope.launch {
|
||||
|
||||
viewModel.saveShoppingListItem(item.copy(name = newName))
|
||||
|
||||
}
|
||||
|
||||
showRenameDialog = false
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (showQuantityDialog) {
|
||||
|
||||
QuantityDialog(
|
||||
|
||||
item = item,
|
||||
|
||||
onDismiss = { showQuantityDialog = false },
|
||||
|
||||
onSetQuantity = { quantity ->
|
||||
|
||||
coroutineScope.launch {
|
||||
|
||||
viewModel.saveShoppingListItem(item.copy(quantity = quantity))
|
||||
|
||||
}
|
||||
|
||||
showQuantityDialog = false
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
showMenu = true
|
||||
}
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.detectReorderAfterLongPress(state),
|
||||
onClick = { /* Drag handle, no direct click action */ }
|
||||
) {
|
||||
Icon(Icons.Default.DragHandle, contentDescription = "Drag handle")
|
||||
}
|
||||
Checkbox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = { isChecked ->
|
||||
coroutineScope.launch {
|
||||
viewModel.saveShoppingListItem(item.copy(isChecked = isChecked))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = if (item.quantity != null) "${item.name} (${item.quantity})" else item.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.rename)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showRenameDialog = true
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.change_quantity)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showQuantityDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenameItemDialog(item: ShoppingListItem, onDismiss: () -> Unit, onRename: (String) -> Unit) {
|
||||
var text by remember { mutableStateOf(item.name) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Artikel umbenennen") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
label = { Text("Neuer Name") }
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { onRename(text) }
|
||||
) {
|
||||
Text("Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = onDismiss) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QuantityDialog(item: ShoppingListItem, onDismiss: () -> Unit, onSetQuantity: (String?) -> Unit) {
|
||||
var text by remember { mutableStateOf(item.quantity ?: "") }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Menge ändern") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
label = { Text("Menge (z.B. 1kg, 2 Stück)") }
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { onSetQuantity(text.ifBlank { null }) }
|
||||
) {
|
||||
Text("Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = onDismiss) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
|
||||
fun RenameItemDialog(item: ShoppingListItem, onDismiss: () -> Unit, onRename: (String) -> Unit) {
|
||||
|
||||
var text by remember { mutableStateOf(item.name) }
|
||||
|
||||
AlertDialog(
|
||||
|
||||
onDismissRequest = onDismiss,
|
||||
|
||||
title = { Text(stringResource(R.string.rename_item_title)) },
|
||||
|
||||
text = {
|
||||
|
||||
OutlinedTextField(
|
||||
|
||||
value = text,
|
||||
|
||||
onValueChange = { text = it },
|
||||
|
||||
label = { Text(stringResource(R.string.new_name)) }
|
||||
|
||||
)
|
||||
|
||||
},
|
||||
|
||||
confirmButton = {
|
||||
|
||||
Button(
|
||||
|
||||
onClick = { onRename(text) }
|
||||
|
||||
) {
|
||||
|
||||
Text(stringResource(R.string.save))
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
dismissButton = {
|
||||
|
||||
Button(onClick = onDismiss) {
|
||||
|
||||
Text(stringResource(R.string.cancel))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
|
||||
fun QuantityDialog(item: ShoppingListItem, onDismiss: () -> Unit, onSetQuantity: (String?) -> Unit) {
|
||||
|
||||
var text by remember { mutableStateOf(item.quantity ?: "") }
|
||||
|
||||
AlertDialog(
|
||||
|
||||
onDismissRequest = onDismiss,
|
||||
|
||||
title = { Text(stringResource(R.string.change_quantity)) },
|
||||
|
||||
text = {
|
||||
|
||||
OutlinedTextField(
|
||||
|
||||
value = text,
|
||||
|
||||
onValueChange = { text = it },
|
||||
|
||||
label = { Text(stringResource(R.string.quantity_hint)) }
|
||||
|
||||
)
|
||||
|
||||
},
|
||||
|
||||
confirmButton = {
|
||||
|
||||
Button(
|
||||
|
||||
onClick = { onSetQuantity(text.ifBlank { null }) }
|
||||
|
||||
) {
|
||||
|
||||
Text(stringResource(R.string.save))
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
dismissButton = {
|
||||
|
||||
Button(onClick = onDismiss) {
|
||||
|
||||
Text(stringResource(R.string.cancel))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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.firstOrNull // New import
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -86,7 +86,8 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
isChecked = item.isChecked,
|
||||
listId = item.listId
|
||||
listId = item.listId,
|
||||
displayOrder = item.displayOrder
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +97,31 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
|
||||
suspend fun saveShoppingListItem(item: ShoppingListItem) {
|
||||
if (item.name.isNotBlank()) {
|
||||
noteshopRepository.insertShoppingListItem(item)
|
||||
var currentItem = item
|
||||
if (currentItem.id == 0) {
|
||||
val itemsCount = noteshopRepository.getItemsCount(currentItem.listId)
|
||||
currentItem = currentItem.copy(displayOrder = itemsCount)
|
||||
}
|
||||
noteshopRepository.insertShoppingListItem(currentItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun moveItem(listId: Int, from: Int, to: Int) {
|
||||
viewModelScope.launch {
|
||||
val currentListWithItems = noteshopRepository.getShoppingListWithItemsStream(listId).firstOrNull()
|
||||
currentListWithItems?.let {
|
||||
val items = it.items.toMutableList()
|
||||
// Ensure 'from' and 'to' indices are within bounds
|
||||
if (from >= 0 && from < items.size && to >= 0 && to < items.size) {
|
||||
val movedItem = items.removeAt(from)
|
||||
items.add(to, movedItem)
|
||||
|
||||
val updatedItems = items.mapIndexed { index, item ->
|
||||
item.copy(displayOrder = index)
|
||||
}
|
||||
noteshopRepository.updateShoppingListItems(updatedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,8 +160,8 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
val updatedLists = list.mapIndexed { index, item ->
|
||||
item.shoppingList.copy(displayOrder = index)
|
||||
}
|
||||
updatedLists.forEach {
|
||||
noteshopRepository.updateShoppingList(it)
|
||||
updatedLists.forEach { shoppingList ->
|
||||
noteshopRepository.updateShoppingList(shoppingList)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,13 +195,15 @@ data class ShoppingListItemDetails(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
val isChecked: Boolean = false,
|
||||
val listId: Int = 0
|
||||
val listId: Int = 0,
|
||||
val displayOrder: Int = 0
|
||||
) {
|
||||
fun toShoppingListItem(): ShoppingListItem = ShoppingListItem(
|
||||
id = id,
|
||||
name = name,
|
||||
isChecked = isChecked,
|
||||
listId = listId
|
||||
listId = listId,
|
||||
displayOrder = displayOrder
|
||||
)
|
||||
|
||||
fun isValid(): Boolean {
|
||||
|
||||
@@ -42,4 +42,9 @@
|
||||
<string name="remove_completed_items">Remove completed</string>
|
||||
<string name="no_uncompleted_items">No uncompleted items in this list.</string>
|
||||
<string name="completed">completed</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="change_quantity">Change Quantity</string>
|
||||
<string name="rename_item_title">Rename Item</string>
|
||||
<string name="new_name">New Name</string>
|
||||
<string name="quantity_hint">Quantity (e.g., 1kg, 2 pieces)</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user