Refactor: Überarbeitung der Einkaufslistendetailansicht

- Checkboxen entfernt und durch Antippen der Artikel zum Abhaken ersetzt.
- Drag-and-Drop zum Neuanordnen von Artikeln implementiert.
- Buttons zum Verschieben von Artikeln nach oben/unten hinzugefügt, mit Langdruck zum Verschieben an den Anfang/Ende.
- Menü zum Umbenennen und Festlegen der Menge für ausgewählte Artikel hinzugefügt.
- Build-Fehler und Lint-Warnungen behoben.
This commit is contained in:
2025-10-12 02:08:27 +02:00
parent b53245b643
commit f5c1127191
5 changed files with 137 additions and 33 deletions

View File

@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
@@ -31,9 +32,12 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.LooksOne
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -60,6 +64,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -171,6 +176,38 @@ fun AppShell(
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back), tint = Color.White)
}
},
actions = {
IconButton(onClick = { scope.launch { selectedItem?.let { shoppingListsViewModel.moveItemUp(selectedListId ?: 0, it) } } }, 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), tint = Color.White)
}
IconButton(onClick = { scope.launch { selectedItem?.let { shoppingListsViewModel.moveItemDown(selectedListId ?: 0, it) } } }, 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), tint = Color.White)
}
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = { showMenu = !showMenu }, enabled = selectedItem != null) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.more_options), tint = Color.White)
}
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 })
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)

View File

@@ -13,10 +13,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -30,13 +30,19 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -113,52 +119,64 @@ fun ShoppingListDetailScreen(
if (list.items.isEmpty()) {
Text(text = stringResource(R.string.no_items_in_list))
} else {
val allItems = list.items.sortedBy { it.displayOrder }
val filteredItems = if (showCompletedItems) {
list.items
allItems
} else {
list.items.filter { !it.isChecked }
}.sortedBy { it.displayOrder }
allItems.filter { !it.isChecked }
}
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 {
val state = rememberReorderableLazyListState(onMove = { from, to ->
val reorderedItems = filteredItems.toMutableList().apply {
add(to.index, removeAt(from.index))
}
viewModel.moveItem(listId ?: 0, reorderedItems)
})
LazyColumn(
state = state.listState,
modifier = Modifier
.reorderable(state)
.detectReorderAfterLongPress(state)
) {
items(filteredItems, { it.id }) { item ->
val isSelected = item == selectedItem
Box(
modifier = Modifier
.background(if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
viewModel.onSelectItem(item)
}
ReorderableItem(state, key = item.id) { isDragging ->
val elevation = if (isDragging) 8.dp else 0.dp
val isSelected = item == selectedItem
Box(
modifier = Modifier
.shadow(elevation)
.background(if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
coroutineScope.launch {
viewModel.saveShoppingListItem(item.copy(isChecked = !item.isChecked))
}
},
onLongPress = {
viewModel.onSelectItem(item)
}
)
}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (item.quantity != null) "${item.name} (${item.quantity})" else item.name,
style = MaterialTheme.typography.bodyLarge,
textDecoration = if (item.isChecked) TextDecoration.LineThrough else null,
modifier = Modifier.weight(1f)
)
}
) {
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 = if (item.quantity != null) "${item.name} (${item.quantity})" else item.name,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
}
}
}

View File

@@ -234,6 +234,45 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
}
}
suspend fun moveItemUp(listId: Int, itemToMove: ShoppingListItem) {
noteshopRepository.getShoppingListWithItemsStream(listId).firstOrNull()?.let { listWithItems ->
val currentItems = listWithItems.items.toMutableList()
val index = currentItems.indexOfFirst { it.id == itemToMove.id }
if (index > 0) {
currentItems.removeAt(index)
currentItems.add(index - 1, itemToMove)
val updatedItems = currentItems.mapIndexed { idx, item ->
item.copy(displayOrder = idx)
}
noteshopRepository.updateShoppingListItems(updatedItems)
}
}
}
suspend fun moveItemDown(listId: Int, itemToMove: ShoppingListItem) {
noteshopRepository.getShoppingListWithItemsStream(listId).firstOrNull()?.let { listWithItems ->
val currentItems = listWithItems.items.toMutableList()
val index = currentItems.indexOfFirst { it.id == itemToMove.id }
if (index != -1 && index < currentItems.size - 1) {
currentItems.removeAt(index)
currentItems.add(index + 1, itemToMove)
val updatedItems = currentItems.mapIndexed { idx, item ->
item.copy(displayOrder = idx)
}
noteshopRepository.updateShoppingListItems(updatedItems)
}
}
}
fun moveItem(listId: Int, items: List<ShoppingListItem>) {
viewModelScope.launch {
val updatedItems = items.mapIndexed { index, item ->
item.copy(displayOrder = index)
}
noteshopRepository.updateShoppingListItems(updatedItems)
}
}
companion object {
private const val TIMEOUT_MILLIS = 5_000L

View File

@@ -50,4 +50,9 @@
<string name="back">Zurück</string>
<string name="move_to_top">Nach oben</string>
<string name="move_to_bottom">Nach unten</string>
<string name="more_options">Mehr Optionen</string>
<string name="move_item_up">Element nach oben verschieben</string>
<string name="move_item_down">Element nach unten verschieben</string>
<string name="rename_item">Element umbenennen</string>
<string name="set_quantity">Menge festlegen</string>
</resources>

View File

@@ -50,4 +50,9 @@
<string name="back">Back</string>
<string name="move_to_top">Move to Top</string>
<string name="move_to_bottom">Move to Bottom</string>
<string name="more_options">More options</string>
<string name="move_item_up">Move item up</string>
<string name="move_item_down">Move item down</string>
<string name="rename_item">Rename item</string>
<string name="set_quantity">Set quantity</string>
</resources>