Feat: Implementiere ShoppingListDetailScreen für die Artikelverwaltung

Dieser Commit führt einen neuen Detailbildschirm zur Verwaltung von Artikeln innerhalb einer Einkaufsliste ein.

Die wichtigsten Änderungen umfassen:
- Added `ShoppingListDetailScreen` to display items of a selected shopping list.
- Implemented navigation from `ShoppingListsScreen` to `ShoppingListDetailScreen` upon clicking a list card.
- Updated `MainActivity.kt` to handle navigation state and display a back button for the detail view.
- Enhanced `ShoppingListsViewModel.kt` to provide a stream for a single shopping list with its items.
- Added new string resources for the detail screen.
- Users can now view and toggle the checked status of individual items within a dedicated screen.
This commit is contained in:
2025-10-11 00:49:19 +02:00
parent 7704441137
commit a2c9932d90
6 changed files with 158 additions and 9 deletions

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -48,6 +49,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.lxtools.noteshop.ui.notes.NotesScreen
import de.lxtools.noteshop.ui.notes.NotesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListDetailScreen // New import
import de.lxtools.noteshop.ui.shopping.ShoppingListsScreen
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.theme.NoteshopTheme
@@ -57,6 +59,7 @@ import kotlinx.coroutines.launch
sealed class Screen(val route: String, val titleRes: Int) {
object ShoppingLists : Screen("shopping_lists", R.string.menu_shopping_lists)
object Notes : Screen("notes", R.string.menu_notes)
object ShoppingListDetail : Screen("shopping_list_detail", R.string.menu_shopping_list_detail) // New screen
}
class MainActivity : ComponentActivity() {
@@ -80,10 +83,12 @@ fun AppShell(
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen: Screen by remember { mutableStateOf(Screen.ShoppingLists) }
var selectedListId: Int? by remember { mutableStateOf(null) } // New state for selected list ID
val navigationItems = listOf(
Screen.ShoppingLists,
Screen.Notes
// ShoppingListDetail is not in the drawer menu, it's navigated to from ShoppingListsScreen
)
ModalNavigationDrawer(
@@ -118,6 +123,7 @@ fun AppShell(
selected = screen == currentScreen,
onClick = {
currentScreen = screen
selectedListId = null // Clear selected list when navigating via drawer
scope.launch { drawerState.close() }
}
)
@@ -131,11 +137,23 @@ fun AppShell(
TopAppBar(
title = { Text(stringResource(id = currentScreen.titleRes)) },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.apply { if (isClosed) open() else close() } } }) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu_open)
)
if (currentScreen == Screen.ShoppingListDetail) { // Show back button for detail screen
IconButton(onClick = {
currentScreen = Screen.ShoppingLists
selectedListId = null
}) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(id = R.string.menu_open) // Reusing string, should be "Back"
)
}
} else { // Show menu icon for other screens
IconButton(onClick = { scope.launch { drawerState.apply { if (isClosed) open() else close() } } }) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu_open)
)
}
}
}
)
@@ -151,11 +169,27 @@ fun AppShell(
) {
when (currentScreen) {
is Screen.ShoppingLists -> {
ShoppingListsScreen(viewModel = shoppingListsViewModel)
ShoppingListsScreen(
viewModel = shoppingListsViewModel,
onListClick = { listId -> // Pass navigation lambda
selectedListId = listId
currentScreen = Screen.ShoppingListDetail
}
)
}
is Screen.Notes -> {
NotesScreen(viewModel = notesViewModel)
}
is Screen.ShoppingListDetail -> { // New case for detail screen
ShoppingListDetailScreen(
listId = selectedListId,
viewModel = shoppingListsViewModel, // Reusing ViewModel for now
onBack = {
currentScreen = Screen.ShoppingLists
selectedListId = null
}
)
}
}
}
}

View File

@@ -0,0 +1,94 @@
package de.lxtools.noteshop.ui.shopping
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.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R
import de.lxtools.noteshop.data.ShoppingList
import de.lxtools.noteshop.data.ShoppingListItem
import de.lxtools.noteshop.data.ShoppingListWithItems
import kotlinx.coroutines.launch
@Composable
fun ShoppingListDetailScreen(
listId: Int?,
viewModel: ShoppingListsViewModel,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val coroutineScope = rememberCoroutineScope()
// 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))
} 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))
}
}
)
Text(
text = item.name,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
}
}
}
}
}
}
}

View File

@@ -46,7 +46,11 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShoppingListsScreen(viewModel: ShoppingListsViewModel = viewModel(factory = AppViewModelProvider.Factory), modifier: Modifier = Modifier) {
fun ShoppingListsScreen(
viewModel: ShoppingListsViewModel = viewModel(factory = AppViewModelProvider.Factory),
modifier: Modifier = Modifier,
onListClick: (Int) -> Unit // New parameter
) {
val uiState by viewModel.uiState.collectAsState()
val listDetails by viewModel.listDetails.collectAsState()
val itemDetails by viewModel.itemDetails.collectAsState()
@@ -107,7 +111,8 @@ fun ShoppingListsScreen(viewModel: ShoppingListsViewModel = viewModel(factory =
coroutineScope.launch {
viewModel.deleteItem(it)
}
}
},
modifier = Modifier.clickable { onListClick(listWithItems.shoppingList.id) } // Make card clickable
)
}
}

View File

@@ -6,6 +6,7 @@ import de.lxtools.noteshop.data.NoteshopRepository
import de.lxtools.noteshop.data.ShoppingList
import de.lxtools.noteshop.data.ShoppingListItem
import de.lxtools.noteshop.data.ShoppingListWithItems
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -42,7 +43,12 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
suspend fun saveList() {
if (listDetails.value.isValid()) {
noteshopRepository.insertShoppingList(listDetails.value.toShoppingList())
val currentList = listDetails.value.toShoppingList()
if (currentList.id == 0) {
noteshopRepository.insertShoppingList(currentList)
} else {
noteshopRepository.updateShoppingList(currentList)
}
}
}
@@ -81,6 +87,10 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
_itemDetails.value = ShoppingListItemDetails(listId = listId)
}
fun getShoppingListWithItemsStream(listId: Int): Flow<ShoppingListWithItems?> {
return noteshopRepository.getShoppingListWithItemsStream(listId)
}
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}

View File

@@ -24,4 +24,7 @@
<string name="item_name">Artikelname</string>
<string name="item_checked">Abgehakt</string>
<string name="edit">Bearbeiten</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>
</resources>

View File

@@ -24,4 +24,7 @@
<string name="item_name">Item Name</string>
<string name="item_checked">Checked</string>
<string name="edit">Edit</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>
</resources>