From 55e7abfacc3bdad1dac9fafd84b116756e5a703f Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 1 Mar 2026 20:34:56 +0100 Subject: [PATCH 1/4] Add ingredient-based recipe filter (Restefunktion) - Add ingredient filter to recipe search with multi-select support - Filter shows recipes containing ALL selected ingredients (AND logic) - New IngredientFilter component matching existing filter chip style - Enhanced IngredientPickerDialog with X button in search bar - Clear search text if present, close dialog if empty - Multi-select mode keeps dialog open for multiple selections - Filter displays selected ingredients in chip label Co-Authored-By: Claude Opus 4.6 --- .../src/commonMain/kotlin/model/Recipe.kt | 11 +++- .../new_participant/IngredientPickerDialog.kt | 29 +++++++++- .../IngredientFilterSection.kt | 54 +++++++++++++++++++ .../view/event/new_meal_screen/NewMealPage.kt | 18 ++++++- .../view/event/new_meal_screen/RecipeList.kt | 6 ++- 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/view/event/new_meal_screen/IngredientFilterSection.kt diff --git a/composeApp/src/commonMain/kotlin/model/Recipe.kt b/composeApp/src/commonMain/kotlin/model/Recipe.kt index 0cb211a..c383ec2 100644 --- a/composeApp/src/commonMain/kotlin/model/Recipe.kt +++ b/composeApp/src/commonMain/kotlin/model/Recipe.kt @@ -44,7 +44,8 @@ class Recipe { filterForTime: TimeRange?, filterForRecipeType: RecipeType?, filterForSkillLevel: Range?, - filterForSeason: Season? + filterForSeason: Season?, + filterForIngredients: Set = emptySet() ): Boolean { // Apply Filters if (filterForEatingHabit != null && !dietaryHabit.matches(filterForEatingHabit)) @@ -67,6 +68,14 @@ class Recipe { if (filterForSeason != null && !season.contains(filterForSeason)) return false + // Filter by ingredients (recipe must contain ALL selected ingredients) + if (filterForIngredients.isNotEmpty()) { + val recipeIngredientIds = shoppingIngredients.map { it.ingredientRef }.toSet() + if (!recipeIngredientIds.containsAll(filterForIngredients)) { + return false + } + } + // Filter Text if (name.contains(searchText, ignoreCase = true)) return true diff --git a/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt b/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt index 20a215f..7733254 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt @@ -8,8 +8,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.clickable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -47,6 +49,7 @@ fun IngredientPickerDialog( onDismiss: () -> Unit, modifier: Modifier = Modifier, selectedIngredients: List = emptyList(), + multiSelect: Boolean = false ) { Dialog( onDismissRequest = onDismiss @@ -54,8 +57,10 @@ fun IngredientPickerDialog( IngredientPickerContent( ingredientList = ingredientList, onSelected = onSelected, + onDismiss = onDismiss, modifier = modifier, - selectedIngredients = selectedIngredients + selectedIngredients = selectedIngredients, + multiSelect = multiSelect ) } } @@ -65,8 +70,10 @@ fun IngredientPickerDialog( private fun IngredientPickerContent( ingredientList: List, onSelected: (currency: Ingredient) -> Unit, + onDismiss: () -> Unit, modifier: Modifier = Modifier, selectedIngredients: List = emptyList(), + multiSelect: Boolean = false ) { var query by remember { mutableStateOf("") } var filteredIngredients by remember { mutableStateOf(ingredientList) } @@ -125,6 +132,19 @@ private fun IngredientPickerContent( contentDescription = "Search" ) }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + modifier = Modifier.clickable { + if (query.isEmpty()) { + onDismiss() + } else { + query = "" + } + } + ) + }, modifier = Modifier.padding(bottom = 8.dp) ) } @@ -136,7 +156,12 @@ private fun IngredientPickerContent( ) { IngredientCard( ingredient = it, - onClick = { onSelected(it) }, + onClick = { + onSelected(it) + if (!multiSelect) { + onDismiss() + } + }, modifier = Modifier.fillMaxWidth(), selected = selectedIngredients.contains(it.uid) ) diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/IngredientFilterSection.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/IngredientFilterSection.kt new file mode 100644 index 0000000..c64a348 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/IngredientFilterSection.kt @@ -0,0 +1,54 @@ +package view.event.new_meal_screen + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import model.Ingredient +import view.admin.new_participant.IngredientPickerDialog + +@Composable +fun IngredientFilter( + selectedIngredientIds: Set, + onIngredientsChange: (Set) -> Unit, + allIngredients: List +) { + var showIngredientPicker by remember { mutableStateOf(false) } + var isFilterActive by remember(selectedIngredientIds) { + mutableStateOf(selectedIngredientIds.isNotEmpty()) + } + + // Create a display text for selected ingredients + val displayText = if (isFilterActive) { + val ingredientNames = selectedIngredientIds.mapNotNull { id -> + allIngredients.find { it.uid == id }?.name + } + "Zutat: ${ingredientNames.joinToString(", ")}" + } else { + "Zutaten" + } + + FilterChip( + text = displayText, + isSelected = isFilterActive, + onClick = { showIngredientPicker = true } + ) + + // Ingredient picker dialog (multi-select mode) + if (showIngredientPicker) { + IngredientPickerDialog( + onDismiss = { showIngredientPicker = false }, + onSelected = { ingredient -> + // Toggle selection (add if not selected, remove if already selected) + if (selectedIngredientIds.contains(ingredient.uid)) { + onIngredientsChange(selectedIngredientIds - ingredient.uid) + } else { + onIngredientsChange(selectedIngredientIds + ingredient.uid) + } + }, + selectedIngredients = selectedIngredientIds.toList(), + ingredientList = allIngredients, + multiSelect = true + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt index 9664b79..bec2ef4 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt @@ -89,10 +89,16 @@ fun EditMealScreen( val recipeViewModel: RecipeViewModel = koinInject() val allRecipesState = recipeViewModel.state.collectAsState() val canParticipantEat: ParticipantCanEatRecipe = koinInject() + val ingredientViewModel: view.event.categorized_shopping_list.IngredientViewModel = koinInject() + val ingredientsState = ingredientViewModel.state.collectAsState() NewMealPage( state = state.value, allRecipes = allRecipesState.value, + allIngredients = when (val ingredientsResult = ingredientsState.value) { + is ResultState.Success -> ingredientsResult.data + else -> emptyList() + }, onAction = { action -> when (action) { is EditMealActions.ViewRecipe -> recipeOverviewViewModel.handleAction( @@ -125,6 +131,7 @@ fun EditMealScreen( fun NewMealPage( state: ResultState, allRecipes: List, + allIngredients: List, onAction: (BaseAction) -> Unit, participantCanEatRecipe: (ParticipantTime, RecipeSelection) -> Boolean, getRecipeEatingError: suspend (ParticipantTime, RecipeSelection) -> String?, @@ -140,6 +147,7 @@ fun NewMealPage( onAction = onAction, state = state, allRecipes = allRecipes, + allIngredients = allIngredients, isSearchBarActive = isSearchBarActive, onSearchBarActiveChange = { isSearchBarActive = it } ) @@ -256,6 +264,7 @@ private fun navigateToRecipe( fun SearchBarComponent( state: ResultState, allRecipes: List, + allIngredients: List, onAction: (BaseAction) -> Unit, isSearchBarActive: Boolean, onSearchBarActiveChange: (Boolean) -> Unit @@ -268,6 +277,7 @@ fun SearchBarComponent( var selectedRecipeTypeFilter: RecipeType? by remember { mutableStateOf(null) } var selectedSeasonFilter: Season? by remember { mutableStateOf(null) } var selectedSkillLevelFilter: Range? by remember { mutableStateOf(null) } + var selectedIngredientFilters by remember { mutableStateOf(setOf()) } when (state) { @@ -330,6 +340,11 @@ fun SearchBarComponent( onFilterSelect = { selectedSkillLevelFilter = it }, selectedRecipeType = selectedSkillLevelFilter ) + IngredientFilter( + selectedIngredientIds = selectedIngredientFilters, + onIngredientsChange = { selectedIngredientFilters = it }, + allIngredients = allIngredients + ) } HorizontalDivider(thickness = 4.dp) RecipeList( @@ -346,7 +361,8 @@ fun SearchBarComponent( filterForTime = selectedTimeFilter, filterForRecipeType = selectedRecipeTypeFilter, filterForSkillLevel = selectedSkillLevelFilter, - filterForSeason = selectedSeasonFilter + filterForSeason = selectedSeasonFilter, + selectedIngredientFilters = selectedIngredientFilters ) } ) diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/RecipeList.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/RecipeList.kt index 78734de..b80b5da 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/RecipeList.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/RecipeList.kt @@ -28,7 +28,8 @@ fun RecipeList( filterForTime: TimeRange?, filterForSkillLevel: Range?, filterForSeason: Season?, - filterForRecipeType: RecipeType? + filterForRecipeType: RecipeType?, + selectedIngredientFilters: Set = emptySet() ) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { allRecipes @@ -41,7 +42,8 @@ fun RecipeList( filterForTime, filterForRecipeType, filterForSkillLevel, - filterForSeason + filterForSeason, + selectedIngredientFilters ) } .forEach { recipe -> From 719b8f2c0dd040d3104ccfa025922d34376ef996 Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 1 Mar 2026 20:39:20 +0100 Subject: [PATCH 2/4] Add recipe list view to main menu - New RecipeListScreen with search functionality - Reuses existing RecipeList component from new_meal_screen - Added "Rezeptliste ansehen" menu item in drawer navigation - Search field with clear button (X icon) - Click on recipe navigates to recipe overview screen - Added Routes.RecipeList navigation route Co-Authored-By: Claude Opus 4.6 --- .../view/event/homescreen/DrawerContent.kt | 14 ++- .../event/homescreen/EventOverviewScreen.kt | 3 +- .../event/recipe_list/RecipeListScreen.kt | 99 +++++++++++++++++++ .../view/navigation/RootNavController.kt | 4 + .../kotlin/view/navigation/Routes.kt | 3 + 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt diff --git a/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt b/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt index 339cbe5..dae9765 100644 --- a/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt @@ -29,7 +29,8 @@ fun DrawerContent( onClose: () -> Unit, onLogoutNavigation: () -> Unit, onManageParticipants: () -> Unit, - onManageRecipes: () -> Unit + onManageRecipes: () -> Unit, + onViewRecipes: () -> Unit ) { val login: LoginAndRegister = koinInject() var showConfirmDialog by remember { mutableStateOf(false) } @@ -53,6 +54,17 @@ fun DrawerContent( onManageRecipes() } ) + HorizontalDivider() + Text("Rezepte", fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp)) + HorizontalDivider() + NavigationDrawerItem( + label = { Text(text = "Rezeptliste ansehen") }, + selected = false, + onClick = { + onViewRecipes() + } + ) + HorizontalDivider() Text("Benutzer", fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp)) HorizontalDivider() NavigationDrawerItem( diff --git a/composeApp/src/commonMain/kotlin/view/event/homescreen/EventOverviewScreen.kt b/composeApp/src/commonMain/kotlin/view/event/homescreen/EventOverviewScreen.kt index d1de8d2..17cf57f 100644 --- a/composeApp/src/commonMain/kotlin/view/event/homescreen/EventOverviewScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/event/homescreen/EventOverviewScreen.kt @@ -91,7 +91,8 @@ fun EventOverview( onClose = { scope.launch { drawerState.close() } }, onLogoutNavigation = { onAction(ActionsEventOverview.Logout) }, onManageParticipants = { onAction(NavigationActions.GoToRoute(Routes.ParticipantAdministration)) }, - onManageRecipes = { onAction(NavigationActions.GoToRoute(Routes.RecipeManagement)) } + onManageRecipes = { onAction(NavigationActions.GoToRoute(Routes.RecipeManagement)) }, + onViewRecipes = { onAction(NavigationActions.GoToRoute(Routes.RecipeList)) } ) }, gesturesEnabled = true diff --git a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt new file mode 100644 index 0000000..01dcb0a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt @@ -0,0 +1,99 @@ +package view.event.recipe_list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import org.koin.compose.koinInject +import view.event.actions.NavigationActions +import view.event.actions.handleNavigation +import view.event.new_meal_screen.RecipeList +import view.event.new_meal_screen.RecipeViewModel +import view.navigation.Routes +import view.shared.NavigationIconButton + +@Composable +fun RecipeListScreen(navController: NavHostController) { + val recipeViewModel: RecipeViewModel = koinInject() + val allRecipes by recipeViewModel.state.collectAsStateWithLifecycle() + var searchText by remember { mutableStateOf("") } + + Scaffold( + topBar = { + RecipeListTopBar( + searchText = searchText, + onSearchTextChange = { searchText = it }, + onNavigateBack = { + handleNavigation(navController, NavigationActions.GoBack) + } + ) + } + ) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + RecipeList( + allRecipes = allRecipes, + searchText = searchText, + filterForFoodIntolerance = emptySet(), + filterForEatingHabit = null, + onRecipeSelected = { recipe -> + navController.navigate(Routes.RecipeOverview(recipe.uid)) + }, + filterForPrice = null, + filterForTime = null, + filterForSkillLevel = null, + filterForSeason = null, + filterForRecipeType = null + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecipeListTopBar( + searchText: String, + onSearchTextChange: (String) -> Unit, + onNavigateBack: () -> Unit +) { + Column { + TopAppBar( + title = { Text("Rezeptliste") }, + navigationIcon = { + NavigationIconButton(onLeave = onNavigateBack) + } + ) + OutlinedTextField( + value = searchText, + onValueChange = onSearchTextChange, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + placeholder = { Text("Rezept suchen...") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Suchen" + ) + }, + trailingIcon = { + if (searchText.isNotEmpty()) { + IconButton(onClick = { onSearchTextChange("") }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Löschen" + ) + } + } + }, + singleLine = true + ) + HorizontalDivider() + } +} diff --git a/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt b/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt index 3ac30df..ac5c1ab 100644 --- a/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt +++ b/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt @@ -25,6 +25,7 @@ import view.event.participants.ParticipantSearchBarScreen import view.event.cooking_groups.CookingGroupsScreen import view.event.cooking_groups.ingredients.CookingGroupIngredientScreen import view.event.recepie_overview_screen.RecipeOverviewScreen +import view.event.recipe_list.RecipeListScreen import view.login.LoginScreen import view.login.Register @@ -69,6 +70,9 @@ fun RootNavController( composable { RecipeManagementScreen(navController = navController) } + composable { + RecipeListScreen(navController = navController) + } composable { backStackEntry -> val route = backStackEntry.toRoute() CsvImportScreen( diff --git a/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt b/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt index 564e023..6d9e355 100644 --- a/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt +++ b/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt @@ -62,4 +62,7 @@ interface Routes { @Serializable object RecipeManagement : Routes + + @Serializable + object RecipeList : Routes } From edbec2bb869d8ae10ea48d9881a3fbcc1935242a Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 1 Mar 2026 20:44:20 +0100 Subject: [PATCH 3/4] Fix recipe list menu structure and recipe detail view - Move "Rezepte verwalten" under "Rezepte" section in drawer menu - Fix recipe detail view loading issue when navigating from recipe list - Add InitializeScreenWithRecipeId action to RecipeOverviewViewModel - Create RecipeSelection object with default values (1 portion) when viewing from list - Recipe overview now properly loads when clicked from recipe list Co-Authored-By: Claude Opus 4.6 --- .../view/event/homescreen/DrawerContent.kt | 14 ++++----- .../RecipeOverviewActions.kt | 3 ++ .../RecipeOverviewViewModel.kt | 29 +++++++++++++++++++ .../event/recipe_list/RecipeListScreen.kt | 6 ++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt b/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt index dae9765..c672cf7 100644 --- a/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/view/event/homescreen/DrawerContent.kt @@ -47,13 +47,6 @@ fun DrawerContent( onManageParticipants() } ) - NavigationDrawerItem( - label = { Text(text = "Rezepte verwalten") }, - selected = false, - onClick = { - onManageRecipes() - } - ) HorizontalDivider() Text("Rezepte", fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp)) HorizontalDivider() @@ -64,6 +57,13 @@ fun DrawerContent( onViewRecipes() } ) + NavigationDrawerItem( + label = { Text(text = "Rezepte verwalten") }, + selected = false, + onClick = { + onManageRecipes() + } + ) HorizontalDivider() Text("Benutzer", fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp)) HorizontalDivider() diff --git a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewActions.kt b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewActions.kt index b86e6bc..5b76e3f 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewActions.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewActions.kt @@ -8,5 +8,8 @@ interface RecipeOverviewActions : BaseAction { data class InitializeScreen(val recipeSelection: RecipeSelection, val eventId: String?) : RecipeOverviewActions + data class InitializeScreenWithRecipeId(val recipeId: String) : + RecipeOverviewActions + data class UpdateNumberOfPortions(val newNumberOfPortions: Int) : RecipeOverviewActions } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt index 54d3e75..3e4e8aa 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt @@ -45,6 +45,31 @@ class RecipeOverviewViewModel( } } + private fun initializeViewModelWithRecipeId(recipeId: String) { + _recipeState.value = ResultState.Loading + viewModelScope.launch { + val recipe = eventRepository.getRecipeById(recipeId) + val recipeSelection = RecipeSelection().apply { + recipeRef = recipeId + this.recipe = recipe + selectedRecipeName = recipe.name + guestCount = 1 + } + val calulatedMap = calculatedIngredientAmounts.calculateAmountsForRecipe( + mutableMapOf(), + recipeSelection, + eventId = null + ) + _recipeState.value = ResultState.Success( + RecipeOverviewState( + recipeSelection = recipeSelection, + calculatedIngredientAmounts = calulatedMap.values.toList(), + numberOfPortions = 1 + ) + ) + } + } + private fun changePortionNumber(numberOfPortions: Int) { val state = _recipeState.value.getSuccessData() ?: return viewModelScope.launch { @@ -69,6 +94,10 @@ class RecipeOverviewViewModel( action.eventId ) + is RecipeOverviewActions.InitializeScreenWithRecipeId -> initializeViewModelWithRecipeId( + action.recipeId + ) + is RecipeOverviewActions.UpdateNumberOfPortions -> changePortionNumber(action.newNumberOfPortions) } } diff --git a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt index 01dcb0a..6eda0ac 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt @@ -16,12 +16,15 @@ import view.event.actions.NavigationActions import view.event.actions.handleNavigation import view.event.new_meal_screen.RecipeList import view.event.new_meal_screen.RecipeViewModel +import view.event.recepie_overview_screen.RecipeOverviewActions +import view.event.recepie_overview_screen.RecipeOverviewViewModel import view.navigation.Routes import view.shared.NavigationIconButton @Composable fun RecipeListScreen(navController: NavHostController) { val recipeViewModel: RecipeViewModel = koinInject() + val recipeOverviewViewModel: RecipeOverviewViewModel = koinInject() val allRecipes by recipeViewModel.state.collectAsStateWithLifecycle() var searchText by remember { mutableStateOf("") } @@ -43,6 +46,9 @@ fun RecipeListScreen(navController: NavHostController) { filterForFoodIntolerance = emptySet(), filterForEatingHabit = null, onRecipeSelected = { recipe -> + recipeOverviewViewModel.handleAction( + RecipeOverviewActions.InitializeScreenWithRecipeId(recipe.uid) + ) navController.navigate(Routes.RecipeOverview(recipe.uid)) }, filterForPrice = null, From 3440ca47adefa885db0ac5a7c3c8be632f67070b Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 1 Mar 2026 20:58:28 +0100 Subject: [PATCH 4/4] Add filter functionality to recipe list view - Added all filter options from meal recipe selection: - Essgewohnheit (Eating habits) - Intoleranzen (Food intolerances) - Preis (Price) - Dauer (Time) - Typ (Recipe type) - Saison (Season) - Skill Level - Zutaten (Ingredients) - Reuses existing filter components for consistency - Filter chips display below search bar - All filters work independently and can be combined Co-Authored-By: Claude Opus 4.6 --- .../event/recipe_list/RecipeListScreen.kt | 93 ++++++++++++++++--- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt index 6eda0ac..36c8ef7 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt @@ -2,6 +2,8 @@ package view.event.recipe_list import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search @@ -11,22 +13,46 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController +import model.EatingHabit +import model.FoodIntolerance +import model.Range +import model.RecipeType +import model.Season +import model.TimeRange import org.koin.compose.koinInject import view.event.actions.NavigationActions import view.event.actions.handleNavigation -import view.event.new_meal_screen.RecipeList -import view.event.new_meal_screen.RecipeViewModel +import view.event.categorized_shopping_list.IngredientViewModel +import view.event.new_meal_screen.* import view.event.recepie_overview_screen.RecipeOverviewActions import view.event.recepie_overview_screen.RecipeOverviewViewModel import view.navigation.Routes import view.shared.NavigationIconButton +import view.shared.ResultState +@OptIn(ExperimentalLayoutApi::class) @Composable fun RecipeListScreen(navController: NavHostController) { val recipeViewModel: RecipeViewModel = koinInject() val recipeOverviewViewModel: RecipeOverviewViewModel = koinInject() + val ingredientViewModel: IngredientViewModel = koinInject() + val allRecipes by recipeViewModel.state.collectAsStateWithLifecycle() + val ingredientsState = ingredientViewModel.state.collectAsState() + val allIngredients = when (val state = ingredientsState.value) { + is ResultState.Success -> state.data + else -> emptyList() + } + var searchText by remember { mutableStateOf("") } + var selectedEatingHabitFilter by remember { mutableStateOf(null) } + var selectedFoodIntoleranceFilter by remember { mutableStateOf(emptySet()) } + var selectedPriceFilter by remember { mutableStateOf(null) } + var selectedTimeFilter by remember { mutableStateOf(null) } + var selectedRecipeTypeFilter by remember { mutableStateOf(null) } + var selectedSeasonFilter by remember { mutableStateOf(null) } + var selectedSkillLevelFilter by remember { mutableStateOf(null) } + var selectedIngredientFilters by remember { mutableStateOf(setOf()) } Scaffold( topBar = { @@ -39,29 +65,74 @@ fun RecipeListScreen(navController: NavHostController) { ) } ) { paddingValues -> - Box(modifier = Modifier.padding(paddingValues)) { + Column(modifier = Modifier.padding(paddingValues)) { + // Filter chips + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(1.dp), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) { + EatingHabitFilter( + selectedEatingHabitFilter = selectedEatingHabitFilter, + onFilterSelect = { selectedEatingHabitFilter = it } + ) + FoodIntoleranceFilter( + selectedIntolerances = selectedFoodIntoleranceFilter, + onFiltersChange = { selectedFoodIntoleranceFilter = it } + ) + PriceFilter( + onFilterSelect = { selectedPriceFilter = it }, + selectedPriceFilter = selectedPriceFilter + ) + TimeFilter( + onFilterSelect = { selectedTimeFilter = it }, + selectedTimeFilter = selectedTimeFilter + ) + RecipeTypeFilter( + onFilterSelect = { selectedRecipeTypeFilter = it }, + selectedRecipeType = selectedRecipeTypeFilter + ) + SeasonFilter( + onFilterSelect = { selectedSeasonFilter = it }, + selectedRecipeType = selectedSeasonFilter + ) + SkillLevelFilter( + onFilterSelect = { selectedSkillLevelFilter = it }, + selectedRecipeType = selectedSkillLevelFilter + ) + IngredientFilter( + selectedIngredientIds = selectedIngredientFilters, + onIngredientsChange = { selectedIngredientFilters = it }, + allIngredients = allIngredients + ) + } + + HorizontalDivider(thickness = 2.dp) + + // Recipe list RecipeList( allRecipes = allRecipes, searchText = searchText, - filterForFoodIntolerance = emptySet(), - filterForEatingHabit = null, + filterForFoodIntolerance = selectedFoodIntoleranceFilter, + filterForEatingHabit = selectedEatingHabitFilter, onRecipeSelected = { recipe -> recipeOverviewViewModel.handleAction( RecipeOverviewActions.InitializeScreenWithRecipeId(recipe.uid) ) navController.navigate(Routes.RecipeOverview(recipe.uid)) }, - filterForPrice = null, - filterForTime = null, - filterForSkillLevel = null, - filterForSeason = null, - filterForRecipeType = null + filterForPrice = selectedPriceFilter, + filterForTime = selectedTimeFilter, + filterForSkillLevel = selectedSkillLevelFilter, + filterForSeason = selectedSeasonFilter, + filterForRecipeType = selectedRecipeTypeFilter, + selectedIngredientFilters = selectedIngredientFilters ) } } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun RecipeListTopBar( searchText: String,